/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.dsl;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import org.hamcrest.Matcher;
import org.junit.Test;
import org.mockito.Mockito;

public class LexingMatcherTest {

  LexingMatcher matcher;
  LexingStream stream;

  // token that was just matched
  Token matched;

  // it's just metadata
  TokenType mockType = Mockito.mock(TokenType.class);

  @Test
  public void testCharMatching() throws Exception {
    matcher = LexingMatcher.forChar('+');
    assertMatch("+", "+");
    assertNoMatch("-");
  }

  @Test
  public void testStringMatching() throws Exception {
    matcher = LexingMatcher.forString("foo");
    assertMatch("foo", "foo");

    // matches incomplete words - it's meant for matching double-length operators.
    assertMatch("foobar", "foo");
    assertThat(matched.getLocation(), atIndex(0));
    assertThat(stream.getLocation(), atIndex(3));

    assertNoMatch("FOO"); // case sensitive - you probably wanted to use forKeyword
  }

  @Test
  public void testKeywordMatching() {
    matcher = LexingMatcher.forKeyword("foo");

    // verbatim
    assertMatch("foo", "foo");
    // case insensitive
    assertMatch("FOO", "foo");
    // space separated
    assertMatch("foo bar", "foo");
    assertMatch("foo\tbar", "foo");
    assertMatch("foo\nbar", "foo");

    // operator separated
    assertMatch("foo+bar", "foo");
    assertMatch("foo.bar", "foo");
    assertMatch("foo/bar", "foo");
    assertMatch("foo*bar", "foo");
    assertMatch("foo&bar", "foo");
    assertMatch("foo-bar", "foo");

    // check a bunch of identifiers don't match
    assertNoMatch("foos");
    assertNoMatch("foo0");
    assertNoMatch("foo1");
    assertNoMatch("foo9");
    assertNoMatch("fooO");
    assertNoMatch("foo_foo");
    assertNoMatch("fooFoo");
  }

  @Test
  public void testPatternMatching() throws Exception {
    matcher = LexingMatcher.forPattern("^[a-z]+");

    assertMatch("foo", "foo");
    assertTrue(stream.isEof());

    // check an incomplete string consumes only the matching chars
    assertMatch("foo12", "foo");
    assertFalse(stream.isEof());
    assertThat(stream.getLocation(), atIndex(3));
  }

  @Test
  public void testQuotedStrings() throws Exception {
    matcher = LexingMatcher.forQuotedString("'\"".toCharArray());

    assertMatch("'foo'", "foo");
    // other quotes can be included
    assertMatch("'fo\"o'", "fo\"o");
    // same quote can be escaped
    assertMatch("'fo\\'o'", "fo'o");
    // double quotes work, too
    assertMatch("\"foo\"", "foo");
    assertNoMatch("`foo`");
  }

  @Test(expected = LexerException.class)
  public void testUnterminatedStringLiteralThrowsLexerException() throws Exception {
    matcher = LexingMatcher.forQuotedString('"');
    assertNoMatch("\"foo");
  }

  @Test
  public void testLineComment() throws Exception {
    matcher = LexingMatcher.forLineComment("//");

    assertMatch("//\n", "");

    // can end with eof
    assertMatch("//", "");

    // the comments themselves are thrown away
    assertMatch("// foo your bar\n", "");

    // stream is positioned before the new line.  I could probably consume the newline tbh, but i've preserved the old
    // behaviour (which was like that because newlines needed to be distinct tokens)
    assertMatch("""
        // for bar
        baz
        """, "");
    assertThat(stream.getLocation(), equalTo(new SourceLocation(10, 1, 11)));

    // same thing for \r endings
    assertMatch("// for bar\r\n", "");
    assertThat(stream.getLocation(), equalTo(new SourceLocation(10, 1, 11)));

    // must be the complete prefix
    assertNoMatch("/ this stuff");
  }

  @Test
  public void testWhitespaceMatching() throws Exception {
    matcher = LexingMatcher.forWhitespace(" \t");

    assertMatch(" ", "");
    assertMatch("\t", "");

    assertMatch("  \t\tfoo", "");
    // has eaten all the whitespace, not just the first one
    assertThat(stream.getLocation(), atIndex(4));

    // even though this is whitespace, it's not one of our given whitespace characters
    assertNoMatch("\n");
  }

  private void assertNoMatch(String source) {
    lex(source);
    assertThat(match(), nullValue());
  }

  private Token match() {
    matched = null;
    matched = matcher.match(mockType, stream);

    if (matched != null) {
      assertThat(matched.end, equalTo(stream.getIndex()));
    }

    return matched;
  }

  private void assertMatch(String source, String value) {
    lex(source);
    assertThat(match(), hasProperty("value", equalTo(value)));
  }

  private void lex(String source) {
    stream = new LexingStream(Source.anon(source));
  }

  private Matcher<SourceLocation> atIndex(int expectedIndex) {
    return hasProperty("index", equalTo(expectedIndex));
  }
}
