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

import static nz.org.riskscape.pipeline.ast.PipelineASTMatchers.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import java.net.URI;

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

import nz.org.riskscape.dsl.ProblemCodes;
import nz.org.riskscape.dsl.UnexpectedTokenException;
import nz.org.riskscape.engine.RiskscapeIOException;
import nz.org.riskscape.engine.resource.Resource;
import nz.org.riskscape.pipeline.ast.PipelineDeclarationTest;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.rl.ExpressionParser;
import nz.org.riskscape.rl.ast.BinaryOperation;
import nz.org.riskscape.rl.ast.Constant;
import nz.org.riskscape.rl.ast.FunctionCall;
import nz.org.riskscape.rl.ast.InputChain;
import nz.org.riskscape.rl.ast.ListDeclaration;
import nz.org.riskscape.rl.ast.ParameterToken;
import nz.org.riskscape.rl.ast.PipelineDeclaration;
import nz.org.riskscape.rl.ast.PropertyAccess;
import nz.org.riskscape.rl.ast.StepChain;
import nz.org.riskscape.rl.ast.StepDeclaration;

public class PipelineParserTest {

  ExpressionParser parser = ExpressionParser.INSTANCE;

  private PipelineDeclaration parsed;

  @Test
  public void aSingleStepReference() throws Exception {
    parse("foo");

    assertStep(
      isStepReference("foo")
    );
  }

  @Test
  public void aStepDefinition() throws Exception {
    parse("foo()");

    assertStep(
      isStepDefinition("foo")
    );
  }

  @Test
  public void aStepDefinitionWithAnAName() throws Exception {
    parse("foo() as bar");

    assertStep(
      isStepDefinition("foo", "bar")
    );

    parse("foo() as \"Bar Baz\"");

    assertStep(
      isStepDefinition("foo", "Bar Baz")
    );
  }

  @Test
  public void aStepDefinitionWithAFunkyName() throws Exception {
    parse("foo() as bar");

    assertStep(
      isStepDefinition("foo", "bar")
    );

    parse("foo() as \"Bar Baz\"");

    assertStep(
      isStepDefinition("foo", "Bar Baz")
    );
    assertEquals(parsed.toSource(), "foo() as \"Bar Baz\"");
  }

  @Test
  public void aStepDefinitionWithArguments() throws Exception {
    parse("foo(bar, 'baz', call([me]))");

    assertStep(
      isStepDefinition("foo", null, contains(
        isAnonArg(PropertyAccess.class, "bar"),
        isAnonArg(Constant.class, "'baz'"),
        isAnonArg(FunctionCall.class, "call([me])")
      ))
    );
  }

  @Test
  public void aStepDefinitionWithKeywordArguments() throws Exception {
    parse("foo(bar: baz, great-things: 'happen', \"All The time\": [1, 2, 3]) as named");

    assertStep(
      isStepDefinition("foo", "named", contains(
        keywordArg("bar", PropertyAccess.class, "baz"),
        keywordArg("great-things", Constant.class, "'happen'"),
        keywordArg("All The time", ListDeclaration.class, "[1, 2, 3]")
      ))
    );
  }

  @Test
  public void aStepDefinitionAnWithInputStepRefArgument() throws Exception {
    parse("foo(<- bar)");

    assertStep(
      isStepDefinition("foo", null, contains(
        isAnonArg(InputChain.class, isInputChain(isChainWith(contains(isStepReference("bar")))))
      ))
    );
  }

  @Test
  public void aStepDefinitionWithInputPipeline() throws Exception {
    parse("foo(<- input() -> filter(foo)  as filter_foo -> group())");

    assertStep(
      isStepDefinition("foo", null, contains(
        isAnonArg(InputChain.class,
            isInputChain(isChainWith(
                contains(
                    isStepDefinition("input"),
                    isStepDefinition("filter", "filter_foo", hasSize(1)),
                    isStepDefinition("group")
                )
            ))
        )
      ))
    );
  }

  @Test
  public void aStepDefinitionAnWithKeywordInputStepRefArgument() throws Exception {
    parse("foo(rhs: <- bar)");

    assertStep(
      isStepDefinition("foo", null, contains(
        keywordArg("rhs", InputChain.class, isInputChain(isChainWith(contains(isStepReference("bar")))))
      ))
    );
  }

  @Test
  public void aStepDefinitionAnWithAMixOfArguments() throws Exception {
    parse("foo('foo', thing: bar - baz, rhs: <- bar)");

    assertStep(
      isStepDefinition("foo", null, contains(
        isAnonArg(Constant.class, "'foo'"),
        keywordArg("thing", BinaryOperation.class, "bar - baz"),
        keywordArg("rhs", InputChain.class, isInputChain(isChainWith(contains(isStepReference("bar")))))
      ))
    );
  }

  @Test
  public void canChainReferencesAndDeclaration() throws Exception {
    parse("foo -> bar");

    assertChain(
      isChainWith(contains(isStepReference("foo"), isStepReference("bar")))
    );

    assertEquals(1, parsed.getFirst().getLinkCount());
    assertThat(
        parsed.getFirst().getLink(0),
        isStepLink(isStepReference("foo"), isStepReference("bar"))
    );
  }

  @Test
  public void canHaveAMixOfReferencesAndDeclarations() throws Exception {
    parse("foo() -> bar -> baz()");

    assertChain(
      isChainWith(contains(
        isStepDefinition("foo"),
        isStepReference("bar"),
        isStepDefinition("baz")
      ))
    );

    assertEquals(2, parsed.getFirst().getLinkCount());
    assertThat(
        parsed.getFirst().getLink(0),
        isStepLink(isStepDefinition("foo"), isStepReference("bar"))
    );
    assertThat(
        parsed.getFirst().getLink(1),
        isStepLink(isStepReference("bar"), isStepDefinition("baz"))
    );
  }

  @Test
  public void canHaveMultipleChains() throws Exception {
    parse("afoo -> abar -> abaz  nofoo -> nobar -> nobaz");
    assertEquals(2, parsed.size());

    assertThat(parsed.getFirst(), isChainWith(contains(
        isStepReference("afoo"),
        isStepReference("abar"),
        isStepReference("abaz")
      )
    ));

    assertThat(parsed.getFirst().getLink(1), isStepLink(isStepReference("abar"), isStepReference("abaz")));

    assertThat(parsed.getLast(), isChainWith(contains(
        isStepReference("nofoo"),
        isStepReference("nobar"),
        isStepReference("nobaz")
      )
    ));
  }

  @Test
  public void canHaveReferencesWithNamedInputs() throws Exception {
    parse("foo.name -> bar.gnome");

    assertChain(
      isChainWith(contains(
        isStepReference("foo", "name"),
        isStepReference("bar", "gnome")
      ))
    );
  }

  @Test
  public void canHaveDefinitionsWithNamedInputs() throws Exception {
    parse("foo.name -> bar().gnome");

    assertChain(
      isChainWith(contains(
        isStepReference("foo", "name"),
        isStepDefinition("bar", null, Matchers.<FunctionCall.Argument>empty(), "gnome")
      ))
    );
  }

  @Test
  public void canConditionallyHaveParameterizedStepDefinitions() throws Exception {
    parsed = parser.parsePipelineAllowParameters("foo($bar)");

    assertStep(isStepDefinition("foo", null, contains(isAnonArg(ParameterToken.class, "$bar"))));
  }

  @Test(expected = UnexpectedTokenException.class)
  public void failsToParseIfParameterizedAndNotAllowed() throws Exception {
    parse("foo($bar)");
  }

  @Test
  public void canParseComments() throws Exception {
    parse("""
        # this is a comment
        foo()
        """);
  }

  Resource resource = mock(Resource.class);
  URI uri = URI.create("foo:bar");

  @Test
  public void parseFromAResourceSetsURI() throws Exception {
    when(resource.getLocation()).thenReturn(uri);
    when(resource.getContentAsString()).thenReturn("foo");

    // if successful, get back pipeline with uri set
    assertThat(
        ExpressionParser.parsePipeline(resource).get(),
        allOf(
            PipelineDeclarationTest.equalsSource("foo"),
            hasProperty("metadata", equalTo(PipelineMetadata.ANONYMOUS.withLocation(uri)))
        )
    );
  }

  @Test
  public void parseFromAResourceReturnsProblemIfParsingFails() throws Exception {
    when(resource.getLocation()).thenReturn(uri);
    when(resource.getContentAsString()).thenReturn("foo(");

    // I have started a branch that improves the error message for this - See #1455
    assertThat(
        ExpressionParser.parsePipeline(resource).getProblems(),
        contains(hasProperty("code", equalTo(ProblemCodes.UNEXPECTED_TOKEN)))
    );
  }

  @Test
  public void parseFromAResourceReturnsProblemIfLoadingResource() throws Exception {
    when(resource.getLocation()).thenReturn(uri);
    when(resource.getContentAsString()).thenThrow(new RiskscapeIOException(Problems.foundWith("io")));

    // I have started a branch that improves the error message for this - See #1455
    assertThat(
        ExpressionParser.parsePipeline(resource).getProblems(),
        contains(Problems.foundWith("io"))
    );
  }

  private void assertStep(Matcher<StepDeclaration> stepDefinition) {
    assertEquals(parsed.size(), 1);
    assertEquals(parsed.getFirst().size(), 1);

    StepDeclaration step = parsed.getFirst().getFirst();
    assertThat(
      step,
      stepDefinition
    );
  }

  private void assertChain(Matcher<StepChain> chain) {
    assertEquals(parsed.size(), 1);

    assertThat(
      parsed.getFirst(),
      chain
    );
  }

  private PipelineDeclaration parse(String source) {
    this.parsed = parser.parsePipeline(source);
    return parsed;
  }

  public String join(String... lines) {
    return String.join("\n", lines);
  }

}
