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

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

import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

import org.ini4j.Profile.Section;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import nz.org.riskscape.config.ConfigSection;
import nz.org.riskscape.config.ini4j.Ini4jConfig;
import nz.org.riskscape.engine.cli.PipelineCommand;
import nz.org.riskscape.engine.i18n.MutableMessageSource;
import nz.org.riskscape.engine.ini.IniFileModelBuilder2;
import nz.org.riskscape.engine.ini.IniParser;
import nz.org.riskscape.engine.model.IdentifiedModel;
import nz.org.riskscape.engine.test.EngineCommandIntegrationTest;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.wizard.CliPrompter;
import nz.org.riskscape.wizard.ConfigParser;
import nz.org.riskscape.wizard.Survey;
import nz.org.riskscape.wizard.WizardActions;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;
import nz.org.riskscape.wizard.model2.ModelSurvey2;

/**
 * Tests the save actions in the wizard produce valid results
 */
public class WizardActionsTest extends EngineCommandIntegrationTest {

  Survey survey = new ModelSurvey2(new MutableMessageSource());
  ConfigParser parser = new ConfigParser(ignored -> survey);
  CliPrompter cliPrompter = Mockito.mock(CliPrompter.class);
  WizardActions wizardActions;

  Path outputDir;

  @Before
  public void populateProject() {
    // reuse the integration test bookmarks, etc
    populateProject(homes().resolve(ModelRunWizardCommandTest.class.getSimpleName()));
    this.outputDir = getTempDirectory();
    wizardActions = new WizardActions(cliPrompter, outputDir.toUri());
  }

  private Map<String, List<?>> toMap(String modelName, String iniSection) {
    Section section = IniParser.parse(new ByteArrayInputStream(iniSection.getBytes())).get("model " + modelName);
    return IniParser.toMap(section);
  }

  private String toConfig(String modelName, String... iniSectionLines) {
    return "[model " + modelName + "]\n" + String.join("\n", iniSectionLines);
  }

  private ConfigSection parseIni(File outputFile, String modelName) {
    return Ini4jConfig.load(outputFile.toURI(), engine.getResourceFactory())
        .map(c -> c.getSection("model " + modelName).orElse(null))
        .get();
  }

  @Test
  public void canSaveWizardModelAsIniConfig() throws Exception {
    // take a very basic config wizard config
    String initialConfig = toConfig("foo",
        "framework = wizard",
        "version = unknown-wizard-v1",
        "input-exposures.layer = point-assets",
        "input-exposures.geoprocess = false",
        "input-hazards.layer = kaiju_fire",
        "input-hazards.geoprocess = false",
        "sample.hazards-by = CENTROID",
        "analysis.map-hazard = false",
        "analysis.function = is_exposed",
        "report-event-impact.filter = consequence = 1");

    ResultOrProblems<IncrementalBuildState> buildStateOr =
        parser.loadConfig(project.newExecutionContext(), toMap("foo", initialConfig));
    assertFalse(buildStateOr.hasErrors());

    Mockito.when(cliPrompter.readlineWithTitle("Enter a name for a new model ini file")).thenReturn("foo");

    // now save the wizard as INI config ('framework = wizard' is a spurious parameter here,
    // but makes later matching against the generated config easier)
    wizardActions.saveModelIni(buildStateOr.getWithProblemsIgnored());
    File actualOutput = outputDir.resolve("models_foo.ini").toFile();
    assertTrue(actualOutput.exists());

    // check results match what we started with
    List<String> lines = Files.readAllLines(actualOutput.toPath());
    assertEquals(initialConfig, String.join("\n", lines).trim());

    // check we can read the output back as valid INI config
    ConfigSection parsed = parseIni(actualOutput, "foo");
    assertNotNull(parsed);

    // and check we can load that config back again
    ResultOrProblems<IncrementalBuildState> savedStateOr =
        parser.loadConfig(project.newExecutionContext(), ConfigSection.wild(parsed.toMultiMap()));
    assertFalse(savedStateOr.hasErrors());
  }

  @Test
  public void canSaveWizardModelAsRunnablePipeline() throws Exception {
    // a more complicated example which makes use of all the ModelSurvey QuestionSets.
    // This is similar to the aggregate-hazard model in ModelRunWizardCommandTest#canAggregateByHazard()
    String initialConfig = toConfig("foo",
        "survey = model",
        "version = 0.11.0-wizard-v1",
        "input-exposures.layer = nyc_buildings",
        "input-exposures.geoprocess = false",
        "input-hazards.layer = fire-raster",
        "sample.hazards-by = ALL_INTERSECTIONS",
        "analysis.aggregate-hazards = true",
        "analysis.aggregate-hazards-function = max",
        "analysis.function = dummy",
        "report-event-impact.filter = consequence > 0.0",
        "report-event-impact.select[0] = exposure.name as building_name",
        "report-event-impact.select[1] = consequence as consequence");

    ResultOrProblems<IncrementalBuildState> buildStateOr =
        parser.loadConfig(project.newExecutionContext(), toMap("foo", initialConfig));
    assertFalse(buildStateOr.hasErrors());

    // now save the wizard as pipeline DSL (but don't create a model file)
    wizardActions.savePipelineDsl(buildStateOr.get(), "foo", false);
    File expectedOutput = outputDir.resolve("pipeline_foo.txt").toFile();
    assertTrue(expectedOutput.exists());
    // check that the model files was not created
    assertFalse(outputDir.resolve("models_foo.ini").toFile().exists());

    // evaluate/run the pipeline file produced
    PipelineCommand.Eval cmd = setupCommand(new PipelineCommand.Eval());
    cmd.pipelineFile = expectedOutput.toString();
    cmd.runnerOptions.output = outputDir.toString();
    cmd.runnerOptions.replace = true;
    cmd.run();

    // check the pipeline produces the expected results
    List<List<String>> rows = openCsv("event-impact.csv", "building_name", "consequence");
    assertThat(rows, containsInAnyOrder(
        contains(equalTo("The Hayden Planetarium"), equalTo("2.0")),
        contains(equalTo("Statue of Liberty"), equalTo("4.0")),
        contains(equalTo("Natural History Museum"), equalTo("2.0")),
        contains(equalTo("Trump Tower"), equalTo("9.0")),
        contains(equalTo("New York Public Library"), equalTo("3.0"))));
  }


  @Test
  public void canSaveAsPipelineBasedModel() throws Exception {
    // take a very basic config wizard config
    String initialConfig = toConfig("foo",
        "input-exposures.layer = point-assets",
        "input-exposures.geoprocess = false",
        "input-hazards.layer = fire-raster",
        "sample.hazards-by = centroid",
        "analysis.function = is_exposed");

    // load it
    ResultOrProblems<IncrementalBuildState> buildStateOr =
        parser.loadConfig(project.newExecutionContext(), toMap("foo", initialConfig));
    assertFalse(buildStateOr.hasErrors());

    // save the pipeline, and a model file for it
    wizardActions.savePipelineDsl(buildStateOr.get(), "foo", true);
    File expectedOutput = outputDir.resolve("models_pipeline_foo.ini").toFile();
    assertTrue(expectedOutput.exists());

    // and check we can load the written file back again as a pipeline-based model
    ConfigSection section = parseIni(expectedOutput, "foo-pipeline");
    assertNotNull(section);

    // the test has written these files to a temp directory, instead of the project dir.
    // So we need to adjust the relative location that we try to load the pipeline file from
    project.setRelativeTo(expectedOutput.toURI());
    IniFileModelBuilder2 builder = new IniFileModelBuilder2(project);

    // check we can load the pipeline-based model from file, without errors
    ResultOrProblems<IdentifiedModel> modelOr = builder.buildModel("foo-pipeline", section);
    assertFalse("Unexpected errors: " + modelOr.getProblems().toString(), modelOr.hasErrors());
    assertEquals(modelOr.get().getModel().getFramework().getClass().getSimpleName(),
        "ParameterizedPipelineModelFramework");
  }

  @Test
  public void canRenderWizardModelPipeline() throws Exception {
    // load some basic model config
    String config = toConfig("foo",
        "survey = model",
        "input-exposures.layer = point-assets",
        "input-exposures.geoprocess = false",
        "input-hazards.layer = kaiju_fire",
        "input-hazards.geoprocess = false",
        "wizard.question-choice-3 = skip",
        "wizard.question-choice-4 = sample",
        "sample.hazards-by = CENTROID",
        "analysis.map-hazard = false",
        "analysis.function = is_exposed");
    IncrementalBuildState buildState = parser.loadConfig(project.newExecutionContext(), toMap("foo", config)).get();

    // render the pipeline that gets produced
    String rendered = wizardActions.getRenderedPipeline(buildState.getRealizedPipeline());

    // sanity-check the rendered output has a few expected fields
    assertThat(rendered, containsString("Step: exposures"));
    assertThat(rendered, containsString("Parameters:"));
    assertThat(rendered, containsString("relation : 'point-assets'"));
  }
}
