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

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

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.Test;

import nz.org.riskscape.engine.i18n.Messages;
import nz.org.riskscape.engine.pipeline.RealizedPipeline;
import nz.org.riskscape.engine.pipeline.RealizedStep;
import nz.org.riskscape.engine.steps.FilterStep;
import nz.org.riskscape.engine.steps.RelationInputStep;
import nz.org.riskscape.engine.steps.SelectStep;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.rl.ExpressionParser;
import nz.org.riskscape.rl.ast.PipelineDeclaration;

public class PipelineRendererTest extends ApplicationCommandTest {

  PipelineRenderer pipelineRenderer = new PipelineRenderer() {
    @Override
    public java.util.Locale getLocale() {
      return terminal.getLocale();
    };

    @Override
    public Messages getMessages() {
      return messages;
    }
  };

  Map<String, List<?>> params = new HashMap<>();

  public PipelineRendererTest() {
    engine.getPipelineSteps().add(new RelationInputStep(engine));
    engine.getPipelineSteps().add(new SelectStep(engine));
    engine.getPipelineSteps().add(new FilterStep(engine));
  }


  @Test
  public void testParameterLayout() throws IOException {
    params.put("empty-param", Arrays.asList(""));
    params.put("many-params", Arrays.asList("wan", "too", "fwee"));
    params.put("one-expression", Arrays.asList(
        ExpressionParser.INSTANCE.parse("foo && bar"))
    );

    RealizedStep dependency = RealizedStep.named("bar")
        .withParameters(params);

    RealizedStep step = RealizedStep.named("foo").withParameters(params).withDependencies(dependency);

    pipelineRenderer.printStep(step, new Formatter(terminal.getOut()));

    assertThat(outStream.getLines(), contains(
        containsString("Step: foo:[null]"),
        containsString("Inputs: [bar]"),
        containsString("Parameters:"),
        containsString("empty-param : "),
        containsString("many-params : ["),
        containsString(" 1 : wan"),
        containsString(" 1 : too"),
        containsString(" 1 : fwee"),
        containsString("]"),
        containsString("one-expression : foo && bar"), // expression to string isn't the same as toSource
        containsString("Produces:"),
        containsString("Result: EmptyRelation")
    ));
  }

  @Test
  public void canPrintAStruct() throws Exception {
    pipelineRenderer.printStruct(new Formatter(terminal.getOut()), 0, Struct.of("foo", Types.TEXT));

    assertThat(outStream.getLines(), contains("foo => Text"));
  }

  @Test
  public void canPrintAStructWithAnIndent() throws Exception {
    pipelineRenderer.printStruct(new Formatter(terminal.getOut()), 3, Struct.of("foo", Types.TEXT));

    assertThat(outStream.getLines(), contains("   foo => Text"));
  }

  @Test
  public void canPrintANestedStruct() throws Exception {
    pipelineRenderer.printStruct(new Formatter(terminal.getOut()), 0, Struct.of("foo", Struct.of("bar", Types.TEXT)));

    assertThat(outStream.getLines(), contains(
        "foo => ",
        "  bar => Text"
    ));
  }

  @Test
  public void canPrintADeeplyNestedStruct() throws Exception {
    pipelineRenderer.printStruct(
        new Formatter(terminal.getOut()),
        6,
        Struct.of("foo", Struct.of("baz", Struct.of("bazza", Types.TEXT), "bar", Types.TEXT))
    );

    assertThat(outStream.getLines(), contains(
        "      foo => ",
        "        baz => ",
        "          bazza => Text",
        "        bar => Text"
    ));
  }

  @Test
  public void canPrintASimpleTypeInAListInAStruct() throws Exception {
    pipelineRenderer.printStruct(new Formatter(terminal.getOut()), 0,
        Struct.of("foo", RSList.create(Types.TEXT)));

    assertThat(outStream.getLines(), contains(
        "foo => RSList[Text]"
    ));
  }

  @Test
  public void canPrintAStructInAListInAStruct() throws Exception {
    pipelineRenderer.printStruct(new Formatter(terminal.getOut()), 6,
        Struct.of("foo", RSList.create(Struct.of("bar", Types.TEXT))));

    assertThat(outStream.getLines(), contains(
        "      foo => RSList[",
        "        bar => Text",
        "      ]"
    ));
  }

  @Test
  public void canPrintAStructInAListOfListsInAStruct() throws Exception {
    // may never see this but it renders okay
    pipelineRenderer.printStruct(new Formatter(terminal.getOut()), 6,
        Struct.of("foo", RSList.create(RSList.create(Struct.of("bar", Types.TEXT)))));

    assertThat(outStream.getLines(), contains(
        "      foo => RSList[RSList[",
        "        bar => Text",
        "      ]]"
    ));
  }

  @Test
  public void canRenderPipelineWithProblems() throws Exception {
    // this exception's message includes the % format specifier on purpose. Previously the pipeline renderer
    // would barf if a rendered problem contained these characters.
    RuntimeException boom = new RuntimeException("exception with %22");

    List<RealizedStep> realizedSteps = new ArrayList<>();
    RealizedStep step = RealizedStep.named("foo")
        .withProblems(Problems.caught(boom));

    realizedSteps.add(step);
    RealizedPipeline realizedPipeline
        = new RealizedPipeline(project.newExecutionContext(), PipelineDeclaration.EMPTY, realizedSteps);

    pipelineRenderer.printPipeline(realizedPipeline, new Formatter(terminal.getOut()));
    assertThat(outStream.getLines(), contains(
        "Step: foo:[null]",
        "  Parameters: ",
        "  Produces:",
        "  Result: failed",
        "    java.lang.RuntimeException: exception with %22"
    ));
  }

  @Test
  public void canPrintADotGraphOfAPipeline() throws Exception {
    PipelineDeclaration parser = ExpressionParser.INSTANCE
        .parsePipeline("input(value: 'foo') as foo -> select(*) as bar -> filter(false) as baz");

    RealizedPipeline realizedPipeline = project.newExecutionContext().realize(parser);

    ByteArrayOutputStream out = new ByteArrayOutputStream();
    PrintStream printStream = new PrintStream(out);
    pipelineRenderer.graph(realizedPipeline, printStream, "digraph", "my-pipeline", Arrays.asList("compound=true"));
    out.close();
    String graph = out.toString();

    assertThat(
      graph,
      allOf(
        containsString("""
        "foo" -> "bar"[];
        "bar" -> "baz"[];
        """)
      )
     );
  }

}
