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

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

import java.util.Collections;
import java.util.List;

import org.junit.Before;
import org.junit.Test;

import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.pipeline.RealizationInput;
import nz.org.riskscape.engine.pipeline.RealizationInputImpl;
import nz.org.riskscape.engine.pipeline.RealizedPipeline;
import nz.org.riskscape.engine.pipeline.RealizedStep;
import nz.org.riskscape.rl.ExpressionParser;
import nz.org.riskscape.rl.ast.PipelineDeclaration;
import nz.org.riskscape.rl.ast.StepDefinition;

public class ChildPipelineBuilderTest extends ProjectTest {

  private RealizedPipeline parentPipeline;
  private RealizedStep inputStepInParent;

  @Before
  public void setup() {
    // Add necessary steps to the engine
    engine.getPipelineSteps().add(new RelationInputStep(engine));
    engine.getPipelineSteps().add(new SelectStep(engine));
    engine.getPipelineSteps().add(new FilterStep(engine));

    String parentSource = "input(value: 'test_data') as parent";
    parentPipeline = executionContext.realize(
        ExpressionParser.INSTANCE.parsePipeline(parentSource)
    );

    assertFalse("Parent pipeline should realize without failures", parentPipeline.hasFailures());
    inputStepInParent = parentPipeline.getStep("parent").get();
  }

  /**
   * Creates a test RealizationInput with a given parent pipeline and step name.
   */
  private RealizationInput createRealizationInput(
      RealizedPipeline parent,
      String stepName,
      List<RealizedStep> dependencies
  ) {
    return new RealizationInputImpl(
        parent,
        new StepDefinition(stepName),
        stepName,
        dependencies,
        Collections.emptyMap(),
        Collections.emptyMap()
    );
  }

  @Test
  public void canBuildSimpleChildPipeline() {
    // Given: A child pipeline that transforms input
    String childSource = "in -> select({ * })";
    PipelineDeclaration childAst = ExpressionParser.INSTANCE.parsePipeline(childSource);

    RealizationInput input = createRealizationInput(
        parentPipeline,
        "child",
        List.of(inputStepInParent)
    );

    ChildPipelineBuilder builder = new ChildPipelineBuilder(input, childAst);

    // seed the builder with our input step
    builder.addInputStep(inputStepInParent, "in");

    // realize the child against the input step
    RealizedPipeline childRealized = builder.realizeChild();
    assertThat(childRealized.hasFailures(), is(false));
    assertFalse(builder.isChildFailed());

    // combine the child in to the parent's pipeline
    RealizedPipeline combined = builder.combine();
    assertThat(builder.getCombined(), sameInstance(combined));

    // check they have come together
    assertThat(combined.getStep("parent").orElse(null), allOf(
        not(nullValue()),
        // original step hasn't changed
        sameInstance(inputStepInParent)
    ));

    // the step we added is prefixed with the name from the realization input...
    assertTrue(combined.getStep("child.select").isPresent());
    // ...and it depends on the parent
    assertThat(combined.getStep("child.select").get().getDependencies(), contains(inputStepInParent));

    // also contains a well known step named after the input step name, but has to be added explicitly
    assertFalse(combined.getStep("child").isPresent());

    // the child pipeline step is accepted (rather than the combined one) as this tends to be how it's used (so far)
    combined = builder.addWellKnownOutput(childRealized.getStep("select").get(), List.of());

    // here it is
    RealizedStep wellKnown = combined.getStep("child").orElse(null);
    assertThat(wellKnown, not(nullValue()));
    // it's a dependency of that select step
    assertThat(wellKnown.getDependencies(), contains(combined.getStep("child.select").get()));
  }


  @Test
  public void failedChildPipelineAddsFailureStep() {
    String childSource = "in -> causes_a_failure()";
    PipelineDeclaration childAst = ExpressionParser.INSTANCE.parsePipeline(childSource);

    RealizationInput input = createRealizationInput(
        parentPipeline,
        "child",
        List.of(inputStepInParent)
    );

    ChildPipelineBuilder builder = new ChildPipelineBuilder(input, childAst);
    builder.addInputStep(inputStepInParent, "in");

    // try to realize it (won't work)
    builder.realizeChild();
    assertTrue(builder.isChildFailed());

    // we can combine it, but the result is a failure - this is so we can return a realized pipeline back to the user
    // that gives you a clue about what's happening
    RealizedPipeline combined = builder.combine();
    assertTrue(combined.hasFailures());

    // we didn't need to add a well known step
    RealizedStep failureStep = combined.getStep("child").orElse(null);
    assertThat(failureStep, hasProperty("failed", is(true)));
  }
}
