/*
 * 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 org.junit.experimental.categories.Category;

import nz.org.riskscape.engine.test.EngineTestPlugins;
import nz.org.riskscape.engine.test.EngineTestSettings;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.test.TestUsesRemoteData;
import org.junit.Test;

import nz.org.riskscape.engine.cli.ExpressionCommand;
import nz.org.riskscape.engine.test.EngineCommandIntegrationTest;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;

/**
 * Sanity-checks the GEM exemplar earthquake functions. Note that the GEM functions
 * access a damage curve defined in a git repo, so the results  may change over time
 * as the remote git repo gets updated.
 */
@Category(TestUsesRemoteData.class)
@EngineTestPlugins({"defaults", "cpython"})
@EngineTestSettings({"cpython.python3-bin=python3", "cpython.lib-dir=lib"})
public class GemExemplarFunctionsTest extends EngineCommandIntegrationTest {

  // use concrete reinforced here because that exists in all the vulnerability models we use
  public static final String EXAMPLE_BUILDING = "{ material: 'CR', storeys: 2, strength: 0, occupancy: 'RES'}";

  public static final List<String> GEM_FUNCTIONS = Arrays.asList(
          "GEM_Vulnerability_NZ_SA0_3",
          "GEM_Vulnerability_US_SA0_3",
          "GEM_Vulnerability_NZ_PGA"
  );

  public Path gemHome() {
    return Paths.get("..", "..", "exemplars", "functions", "earthquake", "gem");
  }

  @Test
  public void lowShakingYieldsNoImpact() {
    populateProject(gemHome());
    ExpressionCommand.Eval cmd = setupCommand(new ExpressionCommand.Eval());

    for (String function : GEM_FUNCTIONS) {
      // a zero hazard should result in no damage
      cmd.expression = String.format("%s(%s, 0.0, random_seed: 1)", function, EXAMPLE_BUILDING);
      assertThat(getDR(cmd), is(0D));

      // repeat with a hazard so low it's unlikely to cause damage
      cmd.expression = String.format("%s(%s, 0.018, random_seed: 1)", function, EXAMPLE_BUILDING);
      assertThat(getDR(cmd), is(0D));
    }
  }

  @Test
  public void highShakingYieldsImpact() {
    populateProject(gemHome());
    ExpressionCommand.Eval cmd = setupCommand(new ExpressionCommand.Eval());

    for (String function : GEM_FUNCTIONS) {
      // SA0.3 scale is roughly 3 times PGA, but still a hazard intensity of 1.0 should
      // result in some decent damage (> 1%).
      cmd.expression = String.format("%s(%s, 1.0, random_seed: 456)", function, EXAMPLE_BUILDING);
      assertThat(getDR(cmd), greaterThanOrEqualTo(0.01D));

      // 15 is top of the intensity scale
      cmd.expression = String.format("%s(%s, 15.0, random_seed: 456)", function, EXAMPLE_BUILDING);
      assertThat(getDR(cmd), greaterThanOrEqualTo(0.9D));
    }
  }

  @Test
  public void shakingYieldsExpectedDROnAverage() {
    populateProject(gemHome());
    ExpressionCommand.Eval cmd = setupCommand(new ExpressionCommand.Eval());

    // the building type used here is CR/LWAL+CDM+DUL/H2/RES and PGA=1.03124 requires no interpolation.
    // The vulnerability function should use nonstructural=0.344785 and structural=0.153699 (weighted by 0.5 each),
    // which should give an expected combined DR of 0.249242 on average. With 10K samples, we get 0.2452295 on average
    assertThat(averageOverNTimes("GEM_Vulnerability_NZ_PGA", EXAMPLE_BUILDING, "1.03124", 10000),
            closeTo(0.249242D, 0.005D));

    // repeat with SA(0.3). This time the building type is CR/LFM+CDM+DUM/H2/RES and we pick a value that
    // requires interpolation. 0.97458 is halfway between 0.917925 and 1.03124, which should interpolate between
    // structural damages of between 0.0368763 and 0.052694 (0.04478515) and nonstructural damages of between
    // 0.0394983 and 0.055058 (0.04727815). So 0.04478515 * 0.5 + 0.04727815 * 0.5 = 0.04603165 (expected mean DR).
    // Whereas over 10,000 samples, the function produces 0.04490814 on average
    assertThat(averageOverNTimes("GEM_Vulnerability_NZ_SA0_3", EXAMPLE_BUILDING, "0.97458", 10000),
            closeTo(0.04603165D, 0.0015D));
  }

  @Test
  public void randomSeedChangesDR() {
    populateProject(gemHome());
    ExpressionCommand.Eval cmd = setupCommand(new ExpressionCommand.Eval());

    // take 10 samples and check we get different results
    cmd.expression = String.format("map(range(0, 10), "
        + "x -> round(GEM_Vulnerability_NZ_PGA(%s, 0.5, random_seed: x), 5)"
        + ")", EXAMPLE_BUILDING);
    // for CR/LWAL+CDM+DUL/H2/RES, PGA=0.5 produces a mean structural DR of about 0.0168
    assertThat(cmd.run(),
            is("[0.0243, 7.8E-4, 7.0E-5, 0.00708, 0.00662, 2.0E-5, 0.0371, 0.00983, 0.01118, 0.06309]")
    );
  }

  @Test
  public void buildingAttributesChangeResults() {
    populateProject(gemHome());
    ExpressionCommand.Eval cmd = setupCommand(new ExpressionCommand.Eval());

    // changing the strength (0-2 corresponds to L, M, H) means that the overall building type changes
    // between CR/LWAL+CDM+DUL/H2/RES, CR/LWAL+CDM+DUM/H1/RES, and CR/LWAL+CDH+DUH/H1/RES, which changes the DR
    cmd.expression = String.format("map(range(0, 3), "
            + "x -> round(GEM_Vulnerability_NZ_PGA(merge(%s, { strength: x }), 0.5, random_seed: 10), 5)"
            + ")", EXAMPLE_BUILDING);
    // for CR/LWAL+CDM+DUL/H2/RES, PGA=0.5 produces a mean structural DR of about 0.0168
    assertThat(cmd.run(),
            is("[0.04736, 0.00479, 0.00544]")
    );
  }

  @Test
  public void errorIfNoMatchingBuildingType() {
    populateProject(gemHome());
    ExpressionCommand.Eval cmd = setupCommand(new ExpressionCommand.Eval());

    // change the building material to be Earth ('E'), which isn't present in the vulnerability model.
    // (We do exact matching on material, but best effort match on everything else)
    cmd.expression = String.format("GEM_Vulnerability_NZ_PGA(merge(%s, { material: 'E' }), 0.5, random_seed: 10)",
          EXAMPLE_BUILDING);
    Problem problem = (Problem) cmd.run();
    assertThat(render(problem), containsString("Could not find suitable building type for [E,L,H2,RES]"));
  }

  private double averageOverNTimes(String function, String building, String hazard, int nTimes) {
    ExpressionCommand.Eval cmd = setupCommand(new ExpressionCommand.Eval());
    cmd.expression = String.format("mean(map(range(0, %d), x -> %s(%s, %s, random_seed: x)))",
            nTimes, function, building, hazard);
    return getDR(cmd);
  }

  private double getDR(ExpressionCommand.Eval cmd) {
    Object result = cmd.run();
    if (result instanceof Problem prob) {
      System.err.println("ERROR: " + render(prob));
      fail("ERROR: " + render(prob));
    }
    return Double.parseDouble(result.toString());
  }
}
