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


import static org.junit.Assert.*;

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

import org.junit.After;
import org.junit.Before;
import org.junit.experimental.categories.Category;
import org.junit.Rule;

import nz.org.riskscape.engine.PluginProjectTest;
import nz.org.riskscape.engine.TemporaryDirectoryTestHelper;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.function.FunctionMetadata;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import nz.org.riskscape.engine.resource.Resource;
import nz.org.riskscape.engine.rl.RealizedExpression;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.test.RetryRule;

@Category(CPythonTestMarker.class)
public class CPythonBaseTest extends PluginProjectTest implements TemporaryDirectoryTestHelper {

  Path libDir = Paths.get("lib").toAbsolutePath();
  String functionName = "foo";
  CPythonSpawner runner;
  CPythonFunctionFramework framework;
  Path tmpDir;
  CPythonSettings settings;
  List<Problem> realizationProblems;
  IdentifiedFunction built;
  List<Problem> functionProblems;
  Tuple input;
  Resource pythonScript;

  // Cpython tests flap like a flag in the wind. So if they fail we'll retry them up to three times
  // before giving up.
  @Rule
  public RetryRule retry = new RetryRule();

  @Before
  public void setup() throws Exception {
    settings = CPythonSettings.defaults(libDir, "/usr/bin/python");
    runner = new CPythonSpawner(settings, p -> sunkProblems.add(p));
    framework = new CPythonFunctionFramework(runner);
    tmpDir = createTempDirectory("CPython");
    engine.setProblemSink(p -> sunkProblems.add(p));

    // clear out functions/types to then can be added again in retries
    project.getFunctionSet().clear();
    project.getTypeSet().clear();
  }

  @After
  public void teardown() throws Exception {
    remove(tmpDir);
    runner.killAll();
  }

  ResultOrProblems<IdentifiedFunction> makeFunctionOr(String scriptLines, List<Type> argTypes, Type returnType)
      throws Exception {
    // we need a real script here that we can execute
    Path pyScript = writeFile("test.py", tmpDir, scriptLines);
    FunctionMetadata metadata = new FunctionMetadata(functionName, argTypes, returnType, pyScript.toUri());

    pythonScript = project.getEngine().getResourceFactory().load(pyScript.toUri());
    return framework.build(project, metadata, pythonScript);
  }

  /**
   * Builds the given CPython function.
   */
  IdentifiedFunction makeFunction(String scriptLines, List<Type> argTypes, Type returnType) throws Exception {
    return makeFunctionOr(scriptLines, argTypes, returnType).get();
  }

  /**
   * Build and add the function to the project.
   */
  void addFunction(String name, String scriptLines, List<Type> argTypes, Type returnType) throws Exception {
    functionName = name;
    built = makeFunction(scriptLines, argTypes, returnType);
    project.getFunctionSet().add(built);
  }

  /**
   * Helper to execute the CPython function as a RiskScape expression.
   */
  Object evaluate(String unparsedExpression) {
    return evaluate(unparsedExpression, false);
  }

  Object evaluate(String unparsedExpression, boolean allowFailure) {
    try {
      if (input == null) {
        input = Tuple.of(Struct.EMPTY_STRUCT);
      }

      ResultOrProblems<RealizedExpression> realizedOr =
          expressionRealizer.realize(input.getStruct(), unparsedExpression);

      if (!realizedOr.isPresent()) {
        if (allowFailure) {
          this.realizationProblems = realizedOr.getProblems();
          return null;
        } else {
          fail(Problem.debugString(realizedOr.getProblems()));
        }
      }
      return realizedOr.get().evaluate(input);
    } finally {
      // give the reaper thread a chance to drain stderr and put it in to the engine's problem sink
      runner.checkForOutputAndWait();
    }
  }

  /**
   * Helper to call a CPythonFunction directly from it's identified form
   */
  Object call(IdentifiedFunction function, List<Object> args) {
    return function.getRealizable().get()
        .realize(realizationContext, null, function.getArgumentTypes()).get()
        .call(args);
  }

  @Override
  protected void addFunction(String name, String functionDefinition) {
    super.addFunction(name, functionDefinition);

    ResultOrProblems<IdentifiedFunction> functionOr = project.getFunctionSet().getOr(name);
    built = functionOr.orElse(null);
    functionProblems = functionOr.getProblems();
  }
}
