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

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

import org.junit.Test;

import nz.org.riskscape.engine.ArgsProblems;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.function.ArgumentList;
import nz.org.riskscape.engine.rl.BaseExpressionRealizerTest;
import nz.org.riskscape.engine.rl.LanguageFunctions;
import nz.org.riskscape.engine.rl.RealizedExpression;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.rl.ast.ExpressionProblems;

public class SwitchFunctionTest extends BaseExpressionRealizerTest {

  Struct inputType = Struct.of("foo", Types.INTEGER);
  ArgumentList switchArgs;

  public SwitchFunctionTest() {
    SwitchFunction switchFunction = new SwitchFunction();
    switchArgs = switchFunction.getArguments();

    project.getFunctionSet().add(switchFunction.identified("switch"));
    project.getFunctionSet().add(LanguageFunctions.NULL_OF);
  }

  @Test
  public void willSwitch() throws Exception {
    RealizedExpression re = realizeOnly("switch(on: foo, default: 'wood', cases: ["
        + "{in: [0, 12, 44, 22], return: 'concrete'},{in: [1, 13, 14, 15], return: 'steel'}])",
        inputType).get();

    assertThat(re.getResultType(), is(Types.TEXT));

    assertThat(re.evaluate(tuple("{foo: 0}")), is("concrete"));
    assertThat(re.evaluate(tuple("{foo: 12}")), is("concrete"));
    assertThat(re.evaluate(tuple("{foo: 44}")), is("concrete"));
    assertThat(re.evaluate(tuple("{foo: 22}")), is("concrete"));

    assertThat(re.evaluate(tuple("{foo: 1}")), is("steel"));
    assertThat(re.evaluate(tuple("{foo: 13}")), is("steel"));
    assertThat(re.evaluate(tuple("{foo: 14}")), is("steel"));
    assertThat(re.evaluate(tuple("{foo: 15}")), is("steel"));

    assertThat(re.evaluate(tuple("{foo: 2}")), is("wood"));
    assertThat(re.evaluate(tuple("{foo: 4}")), is("wood"));
    assertThat(re.evaluate(tuple("{foo: 100}")), is("wood"));
  }

  @Test
  public void returnsTheFirstMatch() {
    // returns the first match, we don't check if the user is duplicating values
    RealizedExpression re = realizeOnly("switch(on: foo, default: 'wood', cases: ["
        + "{in: [0, 12, 44, 22], return: 'concrete'},{in: [0, 0, 0], return: 'steel'}])",
        inputType).get();

    assertThat(re.getResultType(), is(Types.TEXT));

    assertThat(re.evaluate(tuple("{foo: 0}")), is("concrete"));
  }

  @Test
  public void defaultValueMayBeDynamic() {
    // default value may be dynamic
    inputType = Struct.of("foo", Types.INTEGER, "bar", Types.TEXT);
    RealizedExpression re = realizeOnly("switch(on: foo, default: bar, cases: ["
        + "{in: [0, 12, 44, 22], return: 'concrete'},{in: [1, 13, 14, 15], return: 'steel'}])",
        inputType).get();
    assertThat(re.evaluate(tuple("{foo: 2, bar: 'barbar'}")), is("barbar"));
  }

  @Test
  public void onValueMayBeNullable() {
    inputType = Struct.of("foo", Nullable.INTEGER);

    RealizedExpression re = realizeOnly("switch(on: foo, default: 'wood', cases: ["
        + "{in: [0, 12, 44, 22], return: 'concrete'},{in: [1, 13, 14, 15], return: 'steel'}])",
        inputType).get();

    assertThat(re.getResultType(), is(Nullable.TEXT));

    assertThat(re.evaluate(Tuple.ofValues(inputType, 0L)), is("concrete"));
    assertThat(re.evaluate(tuple("{foo: null_of('integer')}")), nullValue());
  }

  @Test
  public void mayReturnNullValues() {
    // default can be null
    inputType = Struct.of("foo", Types.INTEGER);
    RealizedExpression re = realizeOnly("switch(on: foo, default: null_of('text'), cases: ["
        + "{in: [0, 12, 44, 22], return: 'concrete'},{in: [1, 13, 14, 15], return: 'steel'}])",
        inputType).get();
    assertThat(re.getResultType(), is(Nullable.TEXT));
    assertThat(re.evaluate(tuple("{foo: 2}")), nullValue());
    assertThat(re.evaluate(tuple("{foo: 12}")), is("concrete"));
  }

  @Test
  public void returnTypesMustMatch() {
    // error if return types not same
    assertThat(realizeOnly("switch(on: foo, default: 'wood', cases: ["
        + "{in: [0], return: 10}])",
        inputType), failedResult(hasAncestorProblem(
            is(SwitchFunction.PROBLEMS.caseMustReturnSameTypeAsDefault(Types.TEXT, Types.INTEGER))
        )));

    assertThat(realizeOnly("switch(on: foo, default: 'wood', cases: ["
        + "{in: [0], return: 10},{in: [1], return: 12.5}])",
        inputType), failedResult(hasAncestorProblem(
            // The different struct types in the cases list means we get a list of floating (integer -> floating)
            // because of ancestor rules
            is(SwitchFunction.PROBLEMS.caseMustReturnSameTypeAsDefault(Types.TEXT, Types.FLOATING))
        )));

    assertThat(realizeOnly("""
        switch(
          on: foo,
          default: 'wood',
          cases: [
            {in: [0], return: 10},
            # return type here is `text` - screws up type of `cases`
            {in: [1], return: '12.5'}
          ]
        )
        """,
        inputType), failedResult(hasAncestorProblem(
          is(ArgsProblems.mismatch(switchArgs.get(2), RSList.create(Types.ANYTHING)))
        )));
  }

  @Test
  public void casesNeedCorrectAttributes() {
    // error if cases have bad attribute names
    assertThat(realizeOnly("switch(on: foo, default: 'wood', cases: ["
        + "{in: [0], result: 10}])",
        inputType), failedResult(hasAncestorProblem(
            // The different struct types in the cases list means we get a list of anything
            is(ArgsProblems.mismatch(switchArgs.get(2),
                RSList.create(Struct.of("in", RSList.create(Types.INTEGER), "result", Types.INTEGER))))
        )));

    // error if cases has additional attributes
    assertThat(realizeOnly("switch(on: foo, default: 'wood', cases: ["
        + "{in: [0], return: 10, foo: 12}])",
        inputType), failedResult(hasAncestorProblem(
            // The different struct types in the cases list means we get a list of anything
            is(ArgsProblems.mismatch(switchArgs.get(2),
                RSList.create(Struct.of("in", RSList.create(Types.INTEGER), "return", Types.INTEGER,
                    "foo", Types.INTEGER))))
        )));

    // error if cases does not have all attributes
    assertThat(realizeOnly("switch(on: foo, default: 'wood', cases: ["
        + "{in: [0]}])",
        inputType), failedResult(hasAncestorProblem(
            // The different struct types in the cases list means we get a list of anything
            is(ArgsProblems.mismatch(switchArgs.get(2),
                RSList.create(Struct.of("in", RSList.create(Types.INTEGER)))))
        )));
  }

  @Test
  public void casesInTypeMustBeSameAsOnType() {
    // error if options different type to on
    assertThat(realizeOnly("switch(on: 'foo', default: 'wood', cases: ["
        + "{in: [0], return: 10}])",
        inputType), failedResult(hasAncestorProblem(
            is(SwitchFunction.PROBLEMS.caseOptionsMustBeSameTypeAsOn(Types.TEXT, Types.INTEGER))
        )));
    assertThat(realizeOnly("switch(on: foo, default: 'wood', cases: ["
        + "{in: [0], return: 10},{in: [1.2], return: 12}])",
        inputType), failedResult(hasAncestorProblem(
            // The different struct types in the cases list means we get a list of floating (Number ancestor rule)
            is(SwitchFunction.PROBLEMS.caseOptionsMustBeSameTypeAsOn(Types.INTEGER, Types.FLOATING))
        )));

    assertThat(realizeOnly(
        """
        switch(
          on: foo,
          default: 'wood',
          cases: [
            {in: [0], return: 10},
            # in is `text`, and can not be coerced to floating
            {in: ['4'], return: 12}
          ]
        )
        """,
        inputType), failedResult(hasAncestorProblem(
            // list of cases devolves to anything because we can't compute a common ancestor - this is a bit of
            // a limitation with the cheap way the switch function works; we could do better by building each case
            // piece at a time, but later
            is(ArgsProblems.mismatch(switchArgs.get(2), RSList.create(Types.ANYTHING)))
        )));
  }

  @Test
  public void casesMustBeConstant() {
    // cases must be constant
    assertThat(realizeOnly("switch(on: foo, default: 10, cases: [{in: [0], return: foo}])", inputType),
        failedResult(hasAncestorProblem(
            isProblem(Problem.Severity.ERROR, ExpressionProblems.class, "constantRequired")
        )));
  }

}
