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

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;
import java.util.Map;

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

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

import nz.org.riskscape.engine.pipeline.Manifest.LocalInfo;
import nz.org.riskscape.engine.pipeline.Manifest.OutputInfo;
import nz.org.riskscape.engine.pipeline.Manifest.StepDescription;
import nz.org.riskscape.engine.pipeline.Manifest.VersionInfo;
import nz.org.riskscape.pipeline.PipelineMetadata;

public class ManifestTest {

  @Test
  public void canPrintPipelineManifest() throws Exception {
    RealizedPipeline realized = mock(RealizedPipeline.class);
    when(realized.getRealizedSteps()).thenReturn(Collections.emptyList());
    PipelineMetadata metadata = new PipelineMetadata(URI.create("https://riskscape.nz/pipeline.txt"), "foo", "bar");
    when(realized.getMetadata()).thenReturn(metadata);

    String mockStart = "2007-12-03T10:15:30";
    String mockEnd = "2007-12-03T10:15:40";

    Manifest manifest = new Manifest(realized);
    //Pickle some manifest items
    manifest.startTime = LocalDateTime.parse(mockStart);
    manifest.finishTime = LocalDateTime.parse(mockEnd);
    manifest.versionInfo.add(new VersionInfo("Engine", "mock-version"));
    manifest.localInfo = new LocalInfo("mock-user", "mock-host");


    StringBuilder sb = new StringBuilder();
    manifest.writeTo(new Formatter(sb));
    String manifestContent = sb.toString();
    assertThat(manifestContent, Matchers.startsWith(
        "Pipeline-ID: foo\n"
        + "Pipeline-Description: bar\n"
        + "Start-Time: 2007-12-03T10:15:30\n"
        + "Finish-Time: 2007-12-03T10:15:40\n"
        + "\n"
        + "Riskscape Version\n"
        + "-----------------\n"
        + "Engine: mock-version\n"
        + "\n"
        + "Host Information\n"
        + "----------------\n"
        + "User: mock-user\n"
        + "Host: mock-host\n"
    ));
    assertThat(manifestContent, containsString("Pipeline Structure\n------------------\n"));
  }

  @Test
  public void canWriteManifest_ThenVerifyIt() throws Exception {
    Path manifestDirectory = Files.createTempDirectory("Riskscape_Manifest_Test");

    Path out1File = manifestDirectory.resolve("file1.txt");
    MessageDigest out1Digest = Manifest.getDigestInstance();
    try (OutputStream fos = Files.newOutputStream(out1File);
        DigestOutputStream dos = new DigestOutputStream(fos, out1Digest)) {
      dos.write("the quick brown fox".getBytes());
    }
    OutputInfo out1Info = new OutputInfo(manifestDirectory.relativize(out1File).toString(),
        Long.toString(out1File.toFile().length()), out1Digest);

    String mockStart = "2007-12-03T10:15:30";
    String mockEnd = "2007-12-03T10:15:40";

    Manifest manifest = new Manifest();
    //Pickle some manifest items
    manifest.steps = Lists.newArrayList(mockStep("my-step", "mock:step", Collections.emptyList()));
    manifest.startTime = LocalDateTime.parse(mockStart);
    manifest.finishTime = LocalDateTime.parse(mockEnd);
    manifest.versionInfo.add(new VersionInfo("Engine", "mock-version"));
    manifest.localInfo = new LocalInfo("mock-user", "mock-host");

    manifest.outputs.add(out1Info);
    manifest.write(new FileOutputStream(new File(manifestDirectory.toFile(), Manifest.MANIFEST_FILE)));

    //Check that manifest text and bin files have been writen
    assertThat(Files.exists(manifestDirectory.resolve(Manifest.MANIFEST_FILE)), is(true));

    //Check that we can verify the manifest
    StringBuilder verificationReportBuilder = new StringBuilder();
    Manifest.verify(manifestDirectory, new Formatter(verificationReportBuilder));
    assertThat(verificationReportBuilder.toString(), is("PASSED file1.txt matches checksum\n"));

    //Lets tamper with outputs
    Files.write(out1File, "random content".getBytes());

    verificationReportBuilder = new StringBuilder();
    Manifest.verify(manifestDirectory, new Formatter(verificationReportBuilder));
    assertThat(verificationReportBuilder.toString(), is("FAILED file1.txt computed checksum does not match\n"));

    //Deleting an output file makes verification file
    Path out1FileCopy = out1File.resolveSibling("copy.txt");
    Files.move(out1File, out1FileCopy);
    verificationReportBuilder = new StringBuilder();
    Manifest.verify(manifestDirectory, new Formatter(verificationReportBuilder));
    assertThat(verificationReportBuilder.toString(), is(
        String.format("FAILED %s No such file or directory%n", out1File)));

    //Put file back for next test.
    Files.move(out1FileCopy, out1File);

    //Messing with manifest.txt is noticed to
    Files.write(manifestDirectory.resolve(Manifest.MANIFEST_FILE), "random content".getBytes());

    verificationReportBuilder = new StringBuilder();
    Manifest.verify(manifestDirectory, new Formatter(verificationReportBuilder));
    assertThat(verificationReportBuilder.toString(), is(
        String.format("FAILED %s computed checksum does not match%n", Manifest.MANIFEST_FILE)));

  }

  @Test
  public void canWriteAndVerifyManifestWithAnyFilename() throws Exception {
    Path manifestDirectory = Files.createTempDirectory("Riskscape_Manifest_Test");

    Path out1File = manifestDirectory.resolve("file1.txt");
    MessageDigest out1Digest = Manifest.getDigestInstance();
    try (OutputStream fos = Files.newOutputStream(out1File);
        DigestOutputStream dos = new DigestOutputStream(fos, out1Digest)) {
      dos.write("the quick brown fox".getBytes());
    }
    OutputInfo out1Info = new OutputInfo(manifestDirectory.relativize(out1File).toString(),
        Long.toString(out1File.toFile().length()), out1Digest);

    String mockStart = "2007-12-03T10:15:30";
    String mockEnd = "2007-12-03T10:15:40";

    Manifest manifest = new Manifest();
    //Pickle some manifest items
    manifest.steps = Lists.newArrayList(mockStep("my-step", "mock:step", Collections.emptyList()));
    manifest.startTime = LocalDateTime.parse(mockStart);
    manifest.finishTime = LocalDateTime.parse(mockEnd);
    manifest.versionInfo.add(new VersionInfo("Engine", "mock-version"));
    manifest.localInfo = new LocalInfo("mock-user", "mock-host");

    manifest.outputs.add(out1Info);
    manifest.write(new FileOutputStream(new File(manifestDirectory.toFile(), "my-manifest.txt")));

    //Check that manifest text and bin files have been writen
    assertThat(Files.exists(manifestDirectory.resolve("my-manifest.txt")), is(true));

    //Check that we can verify the manifest
    StringBuilder verificationReportBuilder = new StringBuilder();
    //We verify the manifest file, not just the directory (which defaults to manifest.txt)
    Manifest.verify(manifestDirectory.resolve("my-manifest.txt"), new Formatter(verificationReportBuilder));
    assertThat(verificationReportBuilder.toString(), is("PASSED file1.txt matches checksum\n"));
  }

  @Test
  public void producesExpectedChecksums() throws Exception {
    MessageDigest md = Manifest.getDigestInstance();
    md.update("the quick brown fox".getBytes());

    //Expected from: echo -n "the quick brown fox" | sha256sum
    assertThat(Manifest.checksum(md), is("9ecb36561341d18eb65484e833efea61edc74b84cf5e6ae1b81c63533e25fc8f"));
  }

  @Test
  public void stepDescriptionHasUsefulToString() {
    StepDescription step = mockStep("my-step", "mock:step", Collections.emptyList());

    assertThat(step.toString(), is(String.format(
        "Step-my-step: mock:step%n"
        + "  Parameters: %n"
        + "    param1 : [value1, value2]%n"
        + "    param2 : [value]%n"
    )));

    step = mockStep("my-step", "mock:step", Lists.newArrayList("step1", "step2"));
    assertThat(step.toString(), is(String.format(
        "Step-my-step: mock:step%n"
        + "  Parameters: %n"
        + "    param1 : [value1, value2]%n"
        + "    param2 : [value]%n"
        + "  Depends On: %n"
        + "    step1%n"
        + "    step2%n"
    )));
  }

  private StepDescription mockStep(String name, String id, List<String> dependsOn) {
    Map<String, List<String>> parameters = ImmutableMap.<String, List<String>>builder()
        .put("param1", Lists.newArrayList("value1", "value2"))
        .put("param2", Lists.newArrayList("value"))
        .build();

    return new StepDescription(name, id, dependsOn, parameters);
  }

}
