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

import static nz.org.riskscape.engine.Assert.*;
import static nz.org.riskscape.engine.Matchers.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.io.IOException;
import java.math.BigDecimal;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;

import org.junit.Before;
import org.junit.Test;

import com.google.common.collect.ImmutableMap;

import nz.org.riskscape.engine.Assert;
import nz.org.riskscape.engine.DummyFunction;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.function.ArgumentList;
import nz.org.riskscape.engine.function.FunctionArgument;
import nz.org.riskscape.engine.function.FunctionCallException;
import nz.org.riskscape.engine.function.FunctionMetadata;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import nz.org.riskscape.engine.function.IdentifiedFunction.Category;
import nz.org.riskscape.engine.function.RiskscapeFunction;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.resource.StringResource;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.types.WithinRange;
import nz.org.riskscape.engine.typeset.CanonicalType;
import nz.org.riskscape.problem.Problem.Severity;
import nz.org.riskscape.problem.ResultOrProblems;

@SuppressWarnings("unchecked")
public class JythonFragilityFunctionTest extends JythonFactoryTest {

  Struct theStruct = Struct.of("foo", Types.TEXT);
  URI fakeUri = URI.create("file:///tmp/foo.py");

  @Before
  public void addTypes() {
    typeSet.add("foo", theStruct);
    typeSet.add("string", Types.TEXT);
  }

  @Test
  public void canBuildAndCallANoArgFunction() throws IOException {
    String script = "from nz.org.riskscape.engine.types import Types\n"
        + "\n"
        + "ID = 'foo'\n"
        + "DESCRIPTION = 'bar'\n"
        + "RETURN_TYPE = Types.TEXT\n"
        + "ARGUMENT_TYPES = []\n"
        + "\n"
        + "def function():\n"
        + "  return 'cool'\n"
        + "\n";

    IdentifiedFunction function = createFromString(script).get();

    assertEquals(function.getReturnType(), Types.TEXT);
    assertEquals("foo", function.getId());
    assertEquals("cool", function.call(Collections.emptyList()));
  }

  @Test
  public void canBuildAndCallAFunctionWithOneIdByType() throws Exception {
    String script = "from nz.org.riskscape.engine.types import Types\n"
        + "\n"
        + "ID = 'foo'\n"
        + "DESCRIPTION = ''\n"
        + "RETURN_TYPE = Types.TEXT\n"
        + "ARGUMENT_TYPES = ['string']\n"
        + "\n"
        + "def function(the_string):\n"
        + "  return the_string\n"
        + "\n";

    IdentifiedFunction function = createFromString(script).get();
    assertEquals("foo", function.getId());

    assertEquals(typeSet.getRequired("string"), function.getArgumentTypes().get(0));

    assertEquals("foo", function.call(Arrays.asList("foo")));
    assertEquals("", function.getDescription());

  }

  @Test
  public void canBuildAndCallAFunctionWithIDSpanningLines() throws Exception {
    String script = "from nz.org.riskscape.engine.types import Types\n"
        + "\n"
        + "ID"
        + "  = 'foo'\n"
        + "DESCRIPTION = ''\n"
        + "RETURN_TYPE = Types.TEXT\n"
        + "ARGUMENT_TYPES = ['string']\n"
        + "\n"
        + "def function(the_string):\n"
        + "  return the_string\n"
        + "\n";

    IdentifiedFunction function = createFromString(script).get();
    assertEquals("foo", function.getId());

    assertEquals(typeSet.getRequired("string"), function.getArgumentTypes().get(0));

    assertEquals("foo", function.call(Arrays.asList("foo")));
    assertEquals("", function.getDescription());
  }

  @Test
  public void canBuildAndCallAFunctionWithOneIdByTypeAndOnePrivateType() throws IOException {
    String script = "from nz.org.riskscape.engine.types import Types\n"
        + "\n"
        + "ID = 'foo'\n"
        + "DESCRIPTION = 'bar'\n"
        + "RETURN_TYPE = Types.TEXT\n"
        + "ARGUMENT_TYPES = [\n"
        + "  'string',\n"
        + "  Types.TEXT\n"
        + "]\n"
        + "\n"
        + "def function(the_string, other_string):\n"
        + "  return \"%s%s\" % (the_string, other_string)\n"
        + "\n";

    IdentifiedFunction function = createFromString(script).get();

    assertEquals(typeSet.getRequired("string"), function.getArgumentTypes().get(0));
    assertEquals(Types.TEXT, function.getArgumentTypes().get(1));
    assertEquals("foobar", function.call(Arrays.asList("foo", "bar")));
  }

  @Test
  public void canBuildAndCallAFunctionWithOneStructArgument() throws IOException {
    String script = "from nz.org.riskscape.engine.types import Types, Struct\n"
        + "\n"
        + "ID = 'foo'\n"
        + "DESCRIPTION = 'bar'\n"
        + "RETURN_TYPE = Types.TEXT\n"
        + "ARGUMENT_TYPES = [\n"
        + "  'foo'\n"
        + "]\n"
        + "\n"
        + "def function(the_foo):\n"
        + "  return \"%s\" % (the_foo['foo'])\n"
        + "\n";


    Tuple value = new Tuple(theStruct).set("foo", "bar");

    IdentifiedFunction function = createFromString(script).get();

    assertEquals(theStruct, function.getArgumentTypes().get(0).getUnwrappedType());

    assertEquals("bar", function.call(Arrays.asList(value)));
  }

  @Test
  public void canBuildAndCallAFunctionWithStructReturnType() throws IOException {
      String script = "from nz.org.riskscape.engine.types import Types, Struct\n"
          + "\n"
          + "ID = 'foo'\n"
          + "DESCRIPTION = 'bar'\n"
          + "RETURN_TYPE = Struct.of('foo', Types.TEXT).build()\n"
          + "ARGUMENT_TYPES = []\n"
          + "\n"
          + "def function(the_foo):\n"
          + "  return {'foo': 'bar'}\n"
          + "\n";

      Tuple value = new Tuple(theStruct).set("foo", "bar");

      IdentifiedFunction function = createFromString(script).get();

      Tuple returned = (Tuple) function.call(Arrays.asList(value));
      assertEquals(value, returned);
  }

  @Test
  public void canBuildAndCallAFunctionWithAnythingReturnTypeReturningDict() throws IOException {
    String script = "from nz.org.riskscape.engine.types import Types, Struct\n"
        + "\n"
        + "ID = 'foo'\n"
        + "DESCRIPTION = 'bar'\n"
        + "RETURN_TYPE = Types.ANYTHING\n"
        + "ARGUMENT_TYPES = []\n"
        + "\n"
        + "def function():\n"
        + "  return {'foo': 'bar'}\n"
        + "\n";


    IdentifiedFunction function = createFromString(script).get();
    Map<?, ?> returned = (Map<?, ?>) function.call(Arrays.asList());
    assertEquals(ImmutableMap.of("foo", "bar"), returned);
  }

  @Test
  public void canBuildAFunctionThatCallsAnotherOne() {
    String script = "from nz.org.riskscape.engine.types import Types\n"
        + "\n"
        + "ID = 'foo'\n" +
        "DESCRIPTION = 'foo'\n" +
        "RETURN_TYPE = Types.ANYTHING\n" +
        "ARGUMENT_TYPES = []\n" +
        "\n" +
        "def function():\n" +
        "  return functions.get('other_function').call()\n" +
        "";

    DummyFunction dummyFunction = new DummyFunction("other_function", Collections.emptyList());
    dummyFunction.pickledLossValue = "foo";

    functionSet.add(dummyFunction);

    IdentifiedFunction function = createFromString(script).get();
    assertEquals("foo", function.call(Collections.emptyList()));
  }

  @Test
  public void canBuildAFunctionThatCallsAnotherJythonOneWithArguments() {
    String childScript = "from nz.org.riskscape.engine.types import Types\n"
        + "\n"
        + "ID = 'strlen'\n" +
        "DESCRIPTION = 'foo'\n" +
        "RETURN_TYPE = Types.INTEGER\n" +
        "ARGUMENT_TYPES = [Types.TEXT]\n" +
        "\n" +
        "def function(string):\n" +
        "  return len(string)\n" +
        "";

    IdentifiedFunction childFunction = createFromString(childScript).get();

    functionSet.add(childFunction);

    String script = "from nz.org.riskscape.engine.types import Types\n"
        + "\n"
        + "ID = 'foo'\n" +
        "DESCRIPTION = 'foo'\n" +
        "RETURN_TYPE = Types.FLOATING\n" +
        "ARGUMENT_TYPES = []\n" +
        "\n" +
        "def function():\n" +
        "  return functions.get('strlen').call('foos')\n" +
        "";

    IdentifiedFunction function = createFromString(script).get();
    assertEquals(4D, function.call(Collections.emptyList()));
  }

  @Test
  public void canBuildAFunctionThatCallsAnotherJythonOneWithAStructInAndOut() {
    String childScript = "from nz.org.riskscape.engine.types import Types\n"
        + "\n"
        + "ID = 'strlen'\n" +
        "DESCRIPTION = 'foo'\n" +
        "RETURN_TYPE = Types.INTEGER\n" +
        "ARGUMENT_TYPES = [Types.TEXT]\n" +
        "\n" +
        "def function(string):\n" +
        "  return len(string)\n" +
        "";

    Struct assetType = (Struct) typeSet.add("asset", Struct.of("material", Types.TEXT).build()).getUnwrappedType();
    Struct lossType = (Struct) typeSet.add("loss", Struct.of("cost", Types.DECIMAL).build()).getUnwrappedType();

    IdentifiedFunction childFunction = createFromString(childScript).get();

    functionSet.add(childFunction);

    String script = "from nz.org.riskscape.engine.types import Types, Struct\n"
        + "\n"
        + "ID = 'foo'\n" +
        "DESCRIPTION = 'foo'\n" +
        "RETURN_TYPE = 'loss'\n" +
        "ARGUMENT_TYPES = ['asset']\n" +
        "\n" +
        "def function(asset):\n" +
        "  return {'cost': functions.get('strlen').call(asset['material'])}\n" +
        "";

    IdentifiedFunction function = createFromString(script).get();


    assertEquals(
        new Tuple(lossType).set("cost", new BigDecimal(4)),
        function.call(Collections.singletonList(new Tuple(assetType).set("material", "wood"))));
  }

  @Test
  public void syntaxErrorsInPythonTurnInToPythonScriptExceptions() {
    String script = "ID = 'broken'\n"
        + "\n"
        + "hi not valid\n";

    assertThat(createFromString(script), failedResult(hasAncestorProblem(
      isProblem(Severity.ERROR, is("SyntaxError: mismatched input 'valid' expecting IN - "
          + "File \"test.py\", line 3, position 7"))
    )));
  }

  @Test
  public void indentationErrorsInPythonTurnInToPythonScriptExceptions() {
    String script =
        "ID = \"foo\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = []\n" +
        "\n" +
        "def function():\n" +
        "  print ARGUMENT_TYPES\n" +
        " print RETURN_TYPE\n" +
        "";


    assertThat(createFromString(script), failedResult(hasAncestorProblem(
      isProblem(Severity.ERROR, is("IndentationError: unindent does not match any outer indentation level - "
          + "File \"test.py\", line 7, position 1"))
    )));
  }

  @Test
  public void nameErrorsInPythonTurnInToPythonScriptExceptions() {
    String script = "TYPE = Struct.Builder.build()\n";

    JythonScriptException ex = Assert.assertThrows(JythonScriptException.class, () -> {
      factory.createType(new StringResource(URI.create("test.py"), script));
    });

    assertThat(render(ex),
        containsString("NameError: name 'Struct' is not defined - File \"test.py\", line 1"));
  }

  @Test
  public void missingIdCausesAPythonScriptException() {
    String script =
        "DESCRIPTION = \"foo\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = []\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";


    JythonScriptException ex = Assert.assertThrows(JythonScriptException.class, () -> {
      createFromString(script);
    });

    assertThat(ex.getCauseProblem(),
      isError(JythonProblems.class, "missingConstant")
    );
  }

  @Test
  public void idNotAStringCausesAPythonScriptException() {
    String script =
        "ID = 1\n" +
        "DESCRIPTION = \"foo\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = []\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    JythonScriptException ex = Assert.assertThrows(JythonScriptException.class, () -> {
      createFromString(script);
    });

    assertThat(
        ex.getCauseProblem(),
          isError(JythonProblems.class, "missingConstant")
    );
  }

  @Test
  public void missingADescriptionIsOkay() {
    String script =
        "ID = \"foo\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = []\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    IdentifiedFunction function = createFromString(script).get();
    assertNotNull(function);
  }

  @Test
  public void descriptionNotAStringCausesAPythonScriptException() {
    String script =
        "ID = \"1\"\n" +
        "DESCRIPTION = 1\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = []\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    assertThat(
        createFromString(script),
        failedResult(
            hasAncestorProblem(isError(JythonProblems.class, "unexpectedType"))
        )
    );
  }

  @Test
  public void missingArgumentTypesCausesAPythonScriptException() {
    String script =
        "ID = \"1\"\n" +
        "DESCRIPTION = \"1\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    assertThat(
        createFromString(script),
        failedResult(
            hasAncestorProblem(isError(JythonProblems.class, "missingConstant"))
        )
    );
  }

  @Test
  public void argumentTypesNotAnArrayCausesAPythonScriptException() {
    String script =
        "ID = \"1\"\n" +
        "DESCRIPTION = \"1\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = 1\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    assertThat(
        createFromString(script),
        failedResult(
            hasAncestorProblem(isError(JythonProblems.class, "unexpectedType"))
        )
    );
  }

  @Test
  public void argumentTypeMembersOfWrongTypeCausesAPythonScriptException() {
    String script =
        "ID = \"1\"\n" +
        "DESCRIPTION = \"1\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = [1]\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    assertThat(
        createFromString(script),
        failedResult(
            hasAncestorProblem(isError(JythonProblems.class, "thingWasWrongType"))
        )
    );
  }

  @Test
  public void missingReturnTypeCausesAPythonScriptException() {
    String script =
        "ID = \"1\"\n" +
        "DESCRIPTION = \"1\"\n" +
        "ARGUMENT_TYPES = []\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    assertThat(
        createFromString(script),
        failedResult(
            hasAncestorProblem(isError(JythonProblems.class, "missingConstant"))
        )
    );
//    JythonScriptException ex = assertThrows(JythonScriptException.class, () -> {
//      createFromString(script);
//    });
//    assertEquals("function is missing RETURN_TYPE", ex.getProblem().getChildren().get(0).getDefaultMessage());
//    assertEquals("function", ex.getTypeOfThing());
  }

  @Test
  public void returnTypeNotATypeOrStringCausesAPythonScriptException() {
    String script =
        "ID = \"1\"\n" +
        "DESCRIPTION = \"1\"\n" +
        "RETURN_TYPE = 1\n" +
        "ARGUMENT_TYPES = []\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    assertThat(
        createFromString(script),
        failedResult(
            hasAncestorProblem(isError(JythonProblems.class, "thingWasWrongType"))
        )
    );
  }

  @Test
  public void missingFunctionCausesAPythonScriptException() {
    String script =
        "ID = \"1\"\n" +
        "DESCRIPTION = \"1\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = []\n" +
        "\n" +
        "def functions():\n" +
        "  pass\n" +
        "";

    assertThat(
        createFromString(script),
        failedResult(
            hasAncestorProblem(isError(JythonProblems.class, "missingFunctionObject"))
        )
    );

  }

  @Test
  public void functionNotAFunctionCausesAPythonScriptException() {
    String script =
        "ID = \"1\"\n" +
        "DESCRIPTION = \"1\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = []\n" +
        "\n" +
        "function = 1\n" +
        "";

    assertThat(
        createFromString(script),
        failedResult(
            hasAncestorProblem(isError(JythonProblems.class, "unexpectedType"))
        )
    );
  }

  @Test
  public void canSpecifyACategory() {
    String script =
        "ID = \"foo\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = []\n" +
        "CATEGORY = \'MISC\'\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    IdentifiedFunction function = createFromString(script).get();
    assertThat(function, is(notNullValue()));
    assertThat(function.getCategory(), is(IdentifiedFunction.Category.MISC));
  }

  @Test
  public void canSpecifyACategory_InMixedCase() {
    String script =
        "ID = \"foo\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = []\n" +
        "CATEGORY = \'miSC\'\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    IdentifiedFunction function = createFromString(script).get();
    assertThat(function, is(notNullValue()));
    assertThat(function.getCategory(), is(IdentifiedFunction.Category.MISC));
  }

  @Test
  public void unknownCategoryResultsInScriptException() {
    String script =
        "ID = \"foo\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = []\n" +
        "CATEGORY = \'UNKNOWN\'\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    assertThat(
        createFromString(script),
        failedResult(
            hasAncestorProblem(isError(GeneralProblems.class, "notAnOption"))
        )
    );
  }

  @Test
  public void categoriesDefaultToRiskModelling() {
    String script =
        "ID = \"foo\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = []\n" +
        //"CATEGORY = \'MISC\'\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    IdentifiedFunction function = createFromString(script).get();
    assertThat(function, is(notNullValue()));
    assertThat(function.getCategory(), is(JythonFactory.DEFAULT_CATEGORY));
  }

  @Test
  public void badTypeLookupErrorGetsDisplayedCorrectly() {
    String script = "from nz.org.riskscape.engine.types import Nullable\n" +
        "ID = \"foo\"\n" +
        "RETURN_TYPE = \"foo\"\n" +
        "ARGUMENT_TYPES = [Nullable.of(typeset.getLinkedType('bad_type'))]\n" +
        "\n" +
        "def function():\n" +
        "  pass\n" +
        "";

    assertThat(createFromString(script), failedResult(hasAncestorProblem(
      is(GeneralProblems.get().noSuchObjectExists("bad_type", CanonicalType.class))
    )));
  }

  @Test
  public void canBuildWithFunctionMetadata() throws IOException {
    String script = "def function(bar):\n"
        + "  return 'cool ' + str(bar)\n"
        + "\n";

    ArgumentList args = ArgumentList.fromArray(new FunctionArgument("bar", Types.INTEGER));
    FunctionMetadata metadata = new FunctionMetadata("foo", args, Types.TEXT, "my cool function",
        Category.RISK_MODELLING, fakeUri);
    IdentifiedFunction function = createFromString(script, metadata).get();

    assertEquals(function.getReturnType(), Types.TEXT);
    assertEquals(function.getArgumentTypes(), Arrays.asList(Types.INTEGER));
    assertEquals(function.getArguments(), args);
    assertEquals(function.getCategory(), Category.RISK_MODELLING);
    assertEquals(function.getDescription(), "my cool function");
    assertEquals("foo", function.getId());
    assertEquals("cool 1", function.call(Arrays.asList(1L)));
  }

  @Test
  public void alwaysNeedNamedFunctionWithMetadata() {
    // the main function should always just be called 'function' when using metadata
    String script = "def bad_name(bar):\n"
        + "  return 'cool ' + str(bar)\n"
        + "\n";

    FunctionMetadata metadata = new FunctionMetadata("foo", Arrays.asList(Types.INTEGER), Types.TEXT, fakeUri);

    assertThat(createFromString(script, metadata),
        failedResult(hasAncestorProblem(is(JythonProblems.get().mustHaveFunction())
    )));
  }

  @Test
  public void mismatchingMetadataResultsInCallError() throws IOException {
    String script = "def function(bar):\n"
        + "  return bar / 2\n"
        + "\n";

    // we say argument-types is text even though it clearly should be a number
    FunctionMetadata metadata = new FunctionMetadata("foo", Arrays.asList(Types.TEXT), Types.FLOATING, fakeUri);

    // adding a function that advertises the wrong types is OK
    IdentifiedFunction function = createFromString(script, metadata).get();
    assertEquals(function.getReturnType(), Types.FLOATING);
    assertEquals(function.getArgumentTypes(), Arrays.asList(Types.TEXT));
    assertEquals(function.getCategory(), Category.UNASSIGNED);
    assertEquals("foo", function.getId());

    // calling the function with the mis-advertised type should result
    assertThrows(FunctionCallException.class, () -> function.call(Arrays.asList("1")));
  }

  @Test
  public void canBuildRealizableFunction() throws IOException {
    String script = "def function(bar):\n"
        + "  return 'cool ' + str(bar)\n"
        + "\n";

    ArgumentList args = ArgumentList.fromArray(new FunctionArgument("bar", Types.FLOATING));
    FunctionMetadata metadata = new FunctionMetadata("foo", args, Types.TEXT, "my cool function",
        Category.RISK_MODELLING, fakeUri);
    IdentifiedFunction function = createFromString(script, metadata).get();

    assertTrue(function.getRealizable().isPresent());
    ResultOrProblems<RiskscapeFunction> result = function.getRealizable().get().realize(realizationContext, null,
        Arrays.asList(new WithinRange(Types.FLOATING, 0D, 10.0D)));
    assertFalse(result.hasProblems());
    assertEquals("cool 1.0", result.get().call(Arrays.asList(1.0)));
  }
}
