/*
 * 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.engine.typexp;

import static nz.org.riskscape.engine.Assert.*;

import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.EnumSet;
import java.util.Iterator;

import org.junit.Test;

import nz.org.riskscape.dsl.UnexpectedTokenException;
import nz.org.riskscape.engine.Assert;
import nz.org.riskscape.rl.TokenTypes;

public class ParserTest {

  @Test
  public void canParseASimpleType() {
    assertInstanceOf(AST.Symbol.class, parse("str"), symbol
        -> assertEquals("str", symbol.ident()));

    assertInstanceOf(AST.Symbol.class, parse("FOO"), symbol
        -> assertEquals("FOO", symbol.ident()));

    assertInstanceOf(AST.Symbol.class, parse("BAR_FOO"), symbol
        -> assertEquals("BAR_FOO", symbol.ident()));
  }

  @Test
  public void canParseAComplexTypeWithNoArgs() throws Exception {
    assertInstanceOf(AST.ComplexType.class, parse("str()"), complexType -> {
      assertEquals("str", complexType.ident());
      assertEquals(0, complexType.argCount());
    });
  }

  @Test
  public void canParseAComplexTypeWithAConstantArg() throws Exception {
    assertInstanceOf(AST.ComplexType.class, parse("str('foo')"), complexType -> {
      assertEquals("str", complexType.ident());
      assertEquals(1, complexType.argCount());
      assertInstanceOf(AST.Constant.class, complexType.arg(0), constant -> {
        assertEquals("foo", constant.toNative());
      });
    });

    assertInstanceOf(AST.ComplexType.class, parse("str('foo bar')"), complexType -> {
      assertEquals("str", complexType.ident());
    });

    assertInstanceOf(AST.ComplexType.class, parse("a_number(-2343847)"), complexType -> {
      assertEquals("a_number", complexType.ident());
      assertInstanceOf(AST.Constant.class, complexType.arg(0), constant -> {
        assertEquals(-2343847L, constant.toNative());
      });
    });

    assertInstanceOf(AST.ComplexType.class, parse("real(0.5453214)"), complexType -> {
      assertEquals("real", complexType.ident());
      assertInstanceOf(AST.Constant.class, complexType.arg(0), constant -> {
        assertEquals(0.5453214F, ((Double) constant.toNative()).doubleValue(), 0.0001);
      });
    });
  }

  @Test
  public void canParseAComplexTypeWithQuotedIdentifiers() throws Exception {
    assertInstanceOf(AST.ComplexType.class, parse("cool(\"foo\", \"bar\")"), complexType -> {
      assertEquals("cool", complexType.ident());
      assertEquals(2, complexType.argCount());

      Iterator<Object> expected = Arrays.<Object>asList(
          "foo",
          "bar"
      ).iterator();

      for (AST arg : complexType.args()) {
        assertInstanceOf(AST.Symbol.class, arg, sym -> {
          assertEquals(expected.next(), sym.ident());
        });
      }
    });
  }

  @Test
  public void canParseAComplexTypeWithLotsOfConstants() throws Exception {
    assertInstanceOf(AST.ComplexType.class, parse("cool(0.2, 'foo', -3434, 'radical parser')"), complexType -> {
      assertEquals("cool", complexType.ident());
      assertEquals(4, complexType.argCount());

      Iterator<Object> expected = Arrays.<Object>asList(
          0.2D,
          "foo",
          -3434L,
          "radical parser"
      ).iterator();

      for (AST arg : complexType.args()) {
        assertInstanceOf(AST.Constant.class, arg, constant -> {
          assertEquals(expected.next(), constant.toNative());
        });
      }
    });
  }

  @Test
  public void canParseAComplexTypeWithIdentsAndConstants() throws Exception {
    assertInstanceOf(AST.ComplexType.class, parse("cool(foo, 1, 'bar')"), complexType -> {
      assertEquals(3, complexType.argCount());

      assertInstanceOf(AST.Symbol.class, complexType.arg(0), symbol -> {
        assertEquals("foo", symbol.ident());
      });

      assertInstanceOf(AST.Constant.class, complexType.arg(1), constant-> {
        assertEquals(1L, constant.toNative());
      });

      assertInstanceOf(AST.Constant.class, complexType.arg(2), constant-> {
        assertEquals("bar", constant.toNative());
      });
    });
  }

  @Test
  public void canParseAComplexTypeWithNestedComplexType() throws Exception {
    assertInstanceOf(AST.ComplexType.class, parse("cool(rad(awesome))"), complexType -> {
      assertEquals("cool", complexType.ident());
      assertEquals(1, complexType.argCount());

      assertInstanceOf(AST.ComplexType.class, complexType.arg(0), nested -> {
        assertEquals("rad", nested.ident());
        assertEquals(1, complexType.argCount());

        assertInstanceOf(AST.Symbol.class, nested.arg(0), symbol -> {
          assertEquals("awesome", symbol.ident());
        });
      });
    });
  }

  @Test
  public void canParseAComplexTypeWithADictionaryArgument() throws Exception {
    assertInstanceOf(AST.ComplexType.class, parse("dict(foo: bar, bar: 'baz', baz: 420)"), complexType -> {
      assertEquals("dict", complexType.ident());
      assertEquals(1, complexType.argCount());

      assertInstanceOf(AST.Dictionary.class, complexType.arg(0), dict -> {

        assertInstanceOf(AST.Symbol.class, dict.get("foo"), symbol -> {
          assertEquals("bar", symbol.ident());
        });

        assertInstanceOf(AST.Constant.class, dict.get("bar"), constant -> {
          assertEquals("baz", constant.toNative());
        });

        assertInstanceOf(AST.Constant.class, dict.get("baz"), constant -> {
          assertEquals(420L, constant.toNative());
        });
      });
    });
  }

  @Test
  public void canParseAComplexTypeWithADictionaryAndNestedComplexType() throws Exception {
    assertInstanceOf(AST.ComplexType.class, parse("dict(foo: bar(foo), bar: 'baz', baz: 420)"), complexType -> {
      assertEquals("dict", complexType.ident());
      assertEquals(1, complexType.argCount());

      assertInstanceOf(AST.Dictionary.class, complexType.arg(0), dict -> {

        assertInstanceOf(AST.ComplexType.class, dict.get("foo"), nested -> {
          assertEquals("bar", nested.ident());
          assertEquals(1, nested.argCount());

          assertInstanceOf(AST.Symbol.class, nested.arg(0), symbol -> {
            assertEquals("foo", symbol.ident());
          });
        });

        assertInstanceOf(AST.Constant.class, dict.get("bar"), constant -> {
          assertEquals("baz", constant.toNative());
        });

        assertInstanceOf(AST.Constant.class, dict.get("baz"), constant -> {
          assertEquals(420L, constant.toNative());
        });
      });
    });
  }

  @Test
  public void typeArgumentsWithoutCommasIsNoGood() throws Exception {
    UnexpectedTokenException ex = Assert.assertThrows(UnexpectedTokenException.class, () -> parse("dict(foo bar baz)"));
    assertEquals(EnumSet.of(TokenTypes.RPAREN, TokenTypes.COMMA), ex.getExpected());
    assertEquals("bar", ex.getGot().value);
  }

  @Test
  public void typeArgumentsWithoutTrailingParenIsNoGood() throws Exception {
    UnexpectedTokenException ex = Assert.assertThrows(UnexpectedTokenException.class, () -> parse("dict(foo "));
    assertEquals(EnumSet.of(TokenTypes.RPAREN, TokenTypes.COMMA), ex.getExpected());
    assertEquals(TokenTypes.EOF, ex.getGot().type);
  }

  @Test
  public void trailingBitsAreNotAllowed() throws Exception {
    UnexpectedTokenException ex = Assert.assertThrows(UnexpectedTokenException.class, ()
        -> parser("dict(foo) extra").parseTypeFromExpression());
    assertEquals(EnumSet.of(TokenTypes.EOF), ex.getExpected());
    assertEquals(TokenTypes.IDENTIFIER, ex.getGot().type);
  }

  @Test
  public void dictionariesMustBeTheLastInArgList() throws Exception {
    UnexpectedTokenException ex = Assert.assertThrows(UnexpectedTokenException.class, ()
        -> parser("dict(foo: bar, baz)").parseTypeFromExpression());
    assertEquals(EnumSet.of(TokenTypes.KEY_IDENTIFIER), ex.getExpected());
    assertEquals(TokenTypes.IDENTIFIER, ex.getGot().type);
  }

  @Test
  public void dictionaryValueMustBeDeclared() throws Exception {
    UnexpectedTokenException ex = Assert.assertThrows(UnexpectedTokenException.class, ()
        -> parser("dict(foo: bar, baz: )").parseTypeFromExpression());
    assertEquals(TokenTypes.RPAREN, ex.getGot().type);
  }

  private AST parse(String expression) {
    return parser(expression).parseAny();
  }

  private Parser parser(String expression) {
    return new Parser(expression);
  }

}
