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

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

import java.util.Optional;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.junit.Test;

import com.google.common.collect.ImmutableMap;

import nz.org.riskscape.dsl.SourceLocation;
import nz.org.riskscape.dsl.Token;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.pipeline.PipelineProblems;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.util.Pair;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ExpressionParser;
import nz.org.riskscape.rl.ast.AST;
import nz.org.riskscape.rl.ast.ParameterToken;
import nz.org.riskscape.rl.ast.PipelineDeclaration;
import nz.org.riskscape.rl.ast.PipelineDeclaration.Found;
import nz.org.riskscape.rl.ast.StepChain;
import nz.org.riskscape.rl.ast.StepDefinition;
import nz.org.riskscape.rl.ast.StepReference;

public class PipelineDeclarationTest {

  private static final String DSL = String.join("\n",
      "input($foo) as in",
      "in -> filter($bar) -> save()"
  );

  // these used to be separate
  private final ExpressionParser pipelineParser = ExpressionParser.INSTANCE;
  private final ExpressionParser expressionParser = ExpressionParser.INSTANCE;

  private final ParameterToken fooParameter = (ParameterToken) expressionParser.parseAllowParameters("$foo");
  private final ParameterToken barParameter = (ParameterToken) expressionParser.parseAllowParameters("$bar");
  private final ParameterToken bazParameter = (ParameterToken) expressionParser.parseAllowParameters("$baz");

  @Test
  public void getsExpectedResultsFromPipelineDecl() {
    PipelineDeclaration pipeline = pipelineParser.parsePipelineAllowParameters(DSL);
    assertThat(pipeline.getFirst(), equalsSource("input($foo) as in"));
    assertThat(pipeline.getLast(), equalsSource("in -> filter($bar) -> save()"));
    assertThat(pipeline, equalsSource(DSL));

    assertThat(pipeline.getChains(), hasSize(2));

    Pair<Token, Token> boundary = pipeline.getBoundary().get();
    assertThat(boundary.getLeft().getValue(), is("input"));
    assertThat(boundary.getRight().getValue(), is(")"));

    // can find steps
    Found found = pipeline.find(step -> step.getIdent().equals("filter")).get();
    assertThat(found.getChain(), equalsSource("in -> filter($bar) -> save()"));
    assertThat(found.getStep(), equalsSource("filter($bar)"));

    assertThat(pipeline.findDefinition("bogus"), is(Optional.empty()));

  }

  @Test
  public void canGetParameters() {
    PipelineDeclaration pipeline = pipelineParser.parsePipelineAllowParameters(String.join("\n",
        "step1($foo, $bar, $bar) -> step2() -> step3($foo)"));
    // can find parameters with the steps - note the order they are first seen is preserved
    assertThat(
        pipeline.getUniqueParameters(),
        contains(
            ParameterToken.named("foo"),
            ParameterToken.named("bar")
        )
    );

    // and the 'all' version
    assertThat(
        pipeline.getAllParameters(),
        contains(
            ParameterToken.named("foo"),
            ParameterToken.named("bar"),
            ParameterToken.named("bar"),
            ParameterToken.named("foo")
        )
    );
  }

  @Test
  public void canReplaceParameters() {
    PipelineDeclaration pipeline = pipelineParser.parsePipelineAllowParameters(String.join("\n",
        "step1($foo, $bar, $foo) -> step2($baz) -> step3($bar)"));

    ResultOrProblems<PipelineDeclaration> withReplacements = pipeline.replaceParameters(
        ImmutableMap.of("foo", expressionParser.parse("'foo-value'"),
            "bar", expressionParser.parse("20"),
            "baz", expressionParser.parse("3.0")));

    assertFalse(withReplacements.hasProblems());
    assertThat(withReplacements.get(),
        equalsSource("step1('foo-value', 20, 'foo-value') -> step2(3.0) -> step3(20)"));
  }

  @Test
  public void cannotReplaceMissingParameters() {
    PipelineDeclaration pipeline = pipelineParser.parsePipelineAllowParameters(String.join("\n",
        "step1($foo, $bar, $foo) -> step2($baz) -> step3($bar)"));

    ResultOrProblems<PipelineDeclaration> withReplacements = pipeline.replaceParameters(
        ImmutableMap.of("foo", expressionParser.parse("'foo-value'"),
            "bar", expressionParser.parse("20")));

    // we didn't have a replacement for $baz so we should get a missing parameter error
    assertTrue(withReplacements.hasProblems());
    assertThat(withReplacements.getProblems(), contains(
        is(Problems.foundWith(bazParameter,  GeneralProblems.required("baz", Parameter.class)))
    ));
  }

  @Test
  public void canReplaceChain() {
    PipelineDeclaration pipeline = pipelineParser.parsePipelineAllowParameters(DSL);

    StepChain replacement = pipelineParser.parsePipeline("input(limit: 10, offset: 3) as in").getFirst();

    PipelineDeclaration updated = pipeline.replace(pipeline.getFirst(), replacement);
    assertThat(updated, equalsSource(String.join("\n",
        "input(limit: 10, offset: 3) as in",
        "in -> filter($bar) -> save()"
    )));
  }

  @Test
  public void canAddAnotherPipelineDecl() {
    PipelineDeclaration pipeline = pipelineParser.parsePipelineAllowParameters(DSL);

    PipelineDeclaration updated = pipeline.add(pipelineParser.parsePipeline(String.join("\n",
        "in -> sort() as sorted",
        "in -> group() as grouped"
    )));
    assertThat(updated, equalsSource(String.join("\n",
        "input($foo) as in",
        "in -> filter($bar) -> save()",
        "in -> sort() as sorted",
        "in -> group() as grouped"
    )));
  }

  @Test
  public void canAddAnotherStepChainl() {
    PipelineDeclaration pipeline = pipelineParser.parsePipelineAllowParameters(DSL);

    StepChain toAdd = pipelineParser.parsePipeline("in -> group() as grouped").getFirst();

    PipelineDeclaration updated = pipeline.add(toAdd);
    assertThat(updated, equalsSource(String.join("\n",
        "input($foo) as in",
        "in -> filter($bar) -> save()",
        "in -> group() as grouped"
    )));
  }

  @Test
  public void checkFailsOnStepRedefinition() {
    PipelineDeclaration pipeline = pipelineParser.parsePipelineAllowParameters(String.join("\n",
        "input($foo) as bar",
        "-> filter(true) as bar"));

    assertThat(pipeline.checkValid(null).getProblems(), contains(
        is(PipelineProblems.get().stepRedefinition("bar", new SourceLocation(15, 1, 16), new SourceLocation(38, 2, 20)))
    ));
  }

  @Test
  public void canReplaceAStep() throws Exception {
    PipelineDeclaration pipeline = pipelineParser.parsePipeline("input() -> foo bar");
    Found found = pipeline.find(step -> step.getIdent().equals("foo")).get();

    // with a reference
    assertThat(
        found.replace(new StepReference("foos")),
        allOf(
            equalsSource("input() -> foos bar"),
            not(sameInstance(pipeline))
        )

    );

    // with a definition
    assertThat(
        found.replace(new StepDefinition("fooy")),
        allOf(
            equalsSource("input() -> fooy() bar"),
            not(sameInstance(pipeline))
        )
    );
  }

  /**
   * Match a pipeline expression by its source, ignoring irrelevant whitespace
   */
  public static Matcher<AST> equalsSource(String expected) {
    return new TypeSafeDiagnosingMatcher<AST>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("expression matches ").appendValue(expected);
      }

      @Override
      protected boolean matchesSafely(AST item, Description mismatchDescription) {
        mismatchDescription.appendText("expression was ").appendValue(item.toSource());
        return equalToCompressingWhiteSpace(expected).matches(item.toSource());
      }
    };
  }
}
