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

import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;

import org.hamcrest.Matcher;
import org.junit.Assert;
import org.junit.Test;

import com.google.common.base.Strings;

import nz.org.riskscape.dsl.Lexer;
import nz.org.riskscape.dsl.LexerException;
import nz.org.riskscape.dsl.SourceLocation;

public class IniParser2Test {

  String source;
  IniFile parsed;

  @Test
  public void simplestIniFile() {
    parse("""
        [foo]
        bar = baz
        """);

    assertThat(
      parsed,
      hasSection("foo", allOf(
        hasKeyValue("bar", "baz")
      ))
    );
  }

  @Test
  public void emptySection() {
    parse("""
        [foo]
        """);

    assertThat(
      parsed,
      hasSection("foo", hasNoMembers())
    );
  }

  @Test
  public void lastValueCanBeTerminatedByEof() {
    parse("""
        [foo]
        bar = baz""");

    assertThat(
      parsed,
      hasSection("foo", allOf(
        hasKeyValue("bar", "baz")
      ))
    );
  }

  @Test
  public void keysAndValuesSeparatedBySpacesOrTabs() {
    parse("""
        [foo]
        bar\t=\tbaz1
        bar =\tbaz2
        bar\t =   baz3
        """);

    assertThat(
      parsed,
      hasSection("foo", allOf(
        hasKeyValue("bar", "baz1"),
        hasKeyValue("bar", "baz2"),
        hasKeyValue("bar", "baz3")
      ))
    );
  }

  @Test
  public void leadingAndTrailingSpacesAreRemoved() {
    parse("""
        [foo]
        bar\t=\tbaz1
        bar =\tbaz2
        bar\t =   baz3
        bar\t =   baz4
        bar\t =   baz5         # with trailing comment
        """);

    assertThat(
      parsed,
      hasSection("foo", allOf(
        hasKeyValue("bar", "baz1"),
        hasKeyValue("bar", "baz2"),
        hasKeyValue("bar", "baz3"),
        hasKeyValue("bar", "baz4"),
        hasKeyValue("bar", "baz5")
      ))
    );
  }

  @Test
  public void funkySectionName() {
    String section = "Some FOO is gonnā get you! 😊";
    parse("""
        [  %s  ]
        foo = bar
        """, section);

    assertThat(
      parsed,
      hasSection(section, hasKeyValue("foo", "bar"))
    );
  }

  @Test
  public void keysCanHaveFunCharactersInThem() {
    parse("""
        [section]
        foo.foo = bar
        foo_foo = baz
        foo-foo = qux
        foo.foo[0] = qax
        """);

    assertThat(
      parsed,
      hasSection("section",  hasMembers(contains(
          keyValue("foo.foo", "bar"),
          keyValue("foo_foo", "baz"),
          keyValue("foo-foo", "qux"),
          keyValue("foo.foo[0]", "qax")
      )))
    );
  }

  @Test
  public void canHaveMultipleKeyValues() {
    parse("""
        [section]
        foo = bar
        bar = baz
        baz = foo
        foo = baz
        """);

    assertThat(
      parsed,
      hasSection("section", hasMembers(contains(
        keyValue("foo", "bar"),
        keyValue("bar", "baz"),
        keyValue("baz", "foo"),
        keyValue("foo", "baz")
      )))
    );
  }

  @Test
  public void canHaveMultipleSections() {
    parse("""
          [foo]
          foo-key = foo-value
          [bar]
          bar-key = bar-value
          [baz]
          baz-key = baz-value
          """);

    assertThat(
      parsed.getSections(),
      contains(
        sectionNamed("foo"),
        sectionNamed("bar"),
        sectionNamed("baz")
      )
    );

    assertThat(
      parsed,
      hasSection("foo", hasKeyValue("foo-key", "foo-value"))
    );

    assertThat(
      parsed,
      hasSection("bar", hasKeyValue("bar-key", "bar-value"))
    );

    assertThat(
      parsed,
      hasSection("baz", hasKeyValue("baz-key", "baz-value"))
    );
  }

  @Test
  public void valuesCanSpanTwoLines() throws Exception {
    parse("""
        [section]
        key = multi \\
        line
        bar = baz
              """);

    assertThat(
        parsed,
        hasSection("section", hasMembers(contains(
            keyValue("key", "multi line"),
            keyValue("bar", "baz")
        )))
    );

    Section section = parsed.getSections().get(0);
    KeyValue keyValue0 = section.getMembers().get(0);
    assertThat(keyValue0.getKeyToken().getLocation(), atLineCol(2, 1));

    KeyValue keyValue1 = section.getMembers().get(1);
    assertThat(keyValue1.getKeyToken().getLocation(), atLineCol(4, 1));

  }

  @Test
  public void valuesCanSpanTwoLinesWithWindowLineEndings() throws Exception {
    parse(
        "[section]\r\n"
        + "key = multi \\\r\n"
        + "line\r\n"
        + "bar = baz\r\n");

    assertThat(
        parsed,
        hasSection("section", hasMembers(contains(
            keyValue("key", "multi line"),
            keyValue("bar", "baz")
        )))
    );

    Section section = parsed.getSections().get(0);
    KeyValue keyValue0 = section.getMembers().get(0);
    assertThat(keyValue0.getKeyToken().getLocation(), atLineCol(2, 1));

    KeyValue keyValue1 = section.getMembers().get(1);
    assertThat(keyValue1.getKeyToken().getLocation(), atLineCol(4, 1));

  }

  @Test
  public void valuesCanSpanManyLines() throws Exception {
    parse("""
        [section]
        key = multi \\
        line \\
        value
        bar = baz
              """);

    assertThat(
        parsed,
        hasSection("section", hasMembers(contains(
            keyValue("key", "multi line value"),
            keyValue("bar", "baz")
        )))
      );
  }

  @Test
  public void valuesSpanningLinesHavePrecedingWhitespaceRemoved() throws Exception {
    parse("""
        [section]
        key = multi \\
              line \\
              value
        bar = baz
              """);

    assertThat(
        parsed,
        hasSection("section", hasMembers(contains(
            keyValue("key", "multi line value"),
            keyValue("bar", "baz")
        )))
      );
  }

  @Test
  public void valuesCanSpanMultipleLinesEvenWithWhitespaceBeforeNewline() {

    // See #1408
    // I can't find any ini specification that does this, but our old ini parser
    // permitted it, so to avoid breaking old projects lets allow it here too

    parse("""
        [section]
        key = multi \\ \s
        line
        bar = baz
             """);

    assertThat(
        parsed,
        hasSection("section", hasMembers(contains(
            keyValue("key", "multi line"),
            keyValue("bar", "baz")
        )))
    );
  }

  @Test
  public void valuesCanBeBlank() throws Exception {
    parse("""
        [section]
        foo =
        bar =
        baz =  \t\t
        qux = qax
        """);

    assertThat(
        parsed,
        hasSection("section", hasMembers(contains(
            keyValue("foo", ""),
            keyValue("bar", ""),
            keyValue("baz", ""),
            keyValue("qux", "qax")
        )))
    );

    // check some edge case stuff
    Section section = parsed.getSections().stream().filter(s -> s.getName().equals("section")).findFirst().get();
    // should not be a single element list with empty string - getAll filters out 'empty' values.
    assertThat(section.getAll("baz"), empty());
    // but the ast of that key *is* there
    assertThat(
        section.getMembers().stream().filter(k -> k.getKey().equals("baz")).findFirst().orElse(null),
        // get value is an empty string, even though in source it is full of whitespace
        allOf(
            hasProperty("value", equalTo("")),
            hasProperty("blank", is(true))
        )
    );
  }

  @Test
  public void canHaveGlobalKeyvalues() throws Exception {
    parse("""
        key = value
        foo = bar
        """);

    assertThat(
        parsed.getAnonymous(),
        contains(
            keyValue("key", "value"),
            keyValue("foo", "bar")
        )
      );
  }

  @Test
  public void canHaveComments() throws Exception {
    parse("""
        # this is a comment
        [section foo] #this is also a comment at the end of a section
           # this comment is preceeded by whitespace
        # key = value
        #baz = foo
        foo = bar\t# this won't affect anything
        # this comment is trailing
        """);

    assertThat(
        parsed,
        hasSection("section foo", hasMembers(contains(
            keyValue("foo", "bar")
        )))
      );
  }

  @Test
  public void trailingCommentsMustBeProceededWhitespace() {
    parse("""
          [section foo]
          foo = a # comment can be preceeded by space
          bar = b\t# comment can be preceeded by tab
          baz = c#not a comment without whitespace
          """);

    assertThat(
        parsed,
        hasSection("section foo", hasMembers(contains(
            keyValue("foo", "a"),
            keyValue("bar", "b"),
            keyValue("baz", "c#not a comment without whitespace")
        )))
      );
  }

  @Test
  public void colonsAreCommentCharacters() throws Exception {
    parse("""
        ; this is a comment
        [section foo]
        ; key = value
        foo = value;with#comment characters
        bar = baz ; trailing comment
        ;comments don't need a space after the comment char
        ; this comment is trailing
        """);

    assertThat(
        parsed,
        hasSection("section foo", hasMembers(contains(
            keyValue("foo", "value;with#comment characters"),
            keyValue("bar", "baz")
        )))
      );
  }

  @Test
  public void canHaveMultilineValuesUsingBackticks() throws Exception {
    parse("""
        [section]
        foo = ```
        bar `is` ``baz
        ```
        bar = baz
        """);

    assertThat(
        parsed,
        hasSection("section", hasMembers(contains(
            keyValue("foo", "bar `is` ``baz\n"),
            keyValue("bar", "baz")
        )))
    );
  }

  @Test
  public void canHaveMultilineValuesUsingDoubleQuotes() throws Exception {
    parse("""
        [section]
        foo = STR
        People often talk about "Jazz", but I bet they don't understand it.
        STR
        """.replaceAll("STR", Strings.repeat("\"", 3)));

    assertThat(
        parsed,
        hasSection("section", hasMembers(contains(
            keyValue("foo", "People often talk about \"Jazz\", but I bet they don't understand it.\n")
        )))
    );
  }

  @Test
  public void canHaveMultilineValuesUsingSingleQuotes() throws Exception {
    parse("""
        [section]
        foo = '''
        Doesn't this excite you?
        '''
        """);

    assertThat(
        parsed,
        hasSection("section", hasMembers(contains(
            keyValue("foo", "Doesn't this excite you?\n")
        )))
    );
  }

  @Test
  public void multilineValuesCanContainOtherMultilineQuoteForms() throws Exception {
    parse("""
        [section]
        foo = ```
        def python():
            '''
            this is my doc string
            '''
            pass
        ```
        """);

    String expected = """
        def python():
            '''
            this is my doc string
            '''
            pass
        """;

    assertThat(
        parsed,
        hasSection("section", hasMembers(contains(
            keyValue("foo", expected)
        )))
    );
  }

  @Test
  public void multilineStringsCanHaveLeadingContent() throws Exception {
    // this is ugly, but I can bet people will want to do it.
    parse("""
        [section]
        foo = '''foo
        some bar
        '''
        """);

    assertThat(
        parsed,
        hasSection("section", hasMembers(contains(
            keyValue("foo", "foo\nsome bar\n")
        )))
    );
  }

  @Test
  public void lexerExceptionIfMutlilineStringNotPreceededByEquals() throws Exception {
    try {
      parse("""
          [function foo]
          framework = expression
          '''
          (oops) -> 'I forgot something...'
          '''
          """);
      Assert.fail("should have thrown lexer exception");
    } catch (LexerException e) {}
  }

  private Matcher<Section> sectionNamed(String name) {
    return hasProperty("name", equalTo(name));
  }

  private Matcher<Section> hasNoMembers() {
    return hasMembers(empty());
  }

  public Lexer<IniTokens> lex(String parseSource, Object...args) {
    return new Lexer<>(IniTokens.tokens(), String.format(parseSource, args));
  }

  public IniFile parse(String parseSource, Object... args) {
    source = String.format(parseSource, args);
    parsed = new IniParser2().parse(source);
    return parsed;
  }

  private Matcher<Section> hasMembers(Matcher<?> matcher) {
    return hasProperty("members", matcher);
  }

  private Matcher<KeyValue> keyValue(String key, String value) {
    return allOf(
      hasProperty("key", equalTo(key)),
      hasProperty("value", equalTo(value))
    );
  }

  private Matcher<Section> hasKeyValue(String key, String value) {
    return hasMembers(hasItem(keyValue(key, value)));
  }

  private Matcher<IniFile> hasSection(String sectionName, Matcher<Section> with) {
    return hasProperty(
      "sections",
      hasItem(
        allOf(
          hasProperty("name", equalTo(sectionName)),
          with
        )
      )
    );
  }

  private Matcher<SourceLocation> atLineCol(int line, int col) {
    return allOf(
        hasProperty("line", equalTo(line)),
        hasProperty("column", equalTo(col))
    );
  }
}
