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

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static nz.org.riskscape.engine.function.DiscreteFunction.builder;

import java.math.BigDecimal;
import java.util.Collections;

import org.junit.Test;

import com.google.common.collect.BoundType;
import com.google.common.collect.Lists;
import com.google.common.collect.Range;

import nz.org.riskscape.engine.Assert;
import nz.org.riskscape.engine.types.Types;


public class DiscreteFunctionTest {

  @Test
  public void canDefineAFunctionWithASinglePoint() {
    DiscreteFunction function = builder().addPoint(1, Maths.newConstant(2)).build();

    assertEquals(Types.INTEGER, function.getReturnType());
    assertEquals(Collections.singletonList(Types.INTEGER), function.getArgumentTypes());
    assertEquals(Long.valueOf(2), function.call(Collections.singletonList(new BigDecimal(1))));
  }

  @Test
  public void canDefineAFunctionWithASingleValueCoveringARange() {
    DiscreteFunction function = builder().addFunction(1, 10, Maths.newConstant(2)).build();

    assertEquals(Types.INTEGER, function.getReturnType());
    assertEquals(Collections.singletonList(Types.INTEGER), function.getArgumentTypes());
    assertEquals(Long.valueOf(2), function.call(Collections.singletonList(new BigDecimal(1))));
    assertEquals(Long.valueOf(2), function.call(Collections.singletonList(new BigDecimal(9))));
  }

  @Test
  public void canDefineAFunctionWithASubfunctionAtAPoint() {
    DiscreteFunction function = builder()
        .addPoint(10, Maths.newPolynomial(new double[] {1, 1}))
        .build();

    assertEquals(Double.valueOf(11), function.call(Collections.singletonList(new BigDecimal(10))));
  }


  @Test
  public void canDefineAFunctionWithASubfunctionAcrossARange() {
    DiscreteFunction function = builder()
        .addFunction(10, 20, Maths.newPolynomial(new double[] {5, 2}))
        .build();

    assertEquals(Double.valueOf(25), function.call(Collections.singletonList(new BigDecimal(10))));
    assertEquals(Double.valueOf(27), function.call(Collections.singletonList(new BigDecimal(11))));
  }

  @Test
  public void canDefineAFunctionWithTwoPointsAndLinearInterpolation() {
    DiscreteFunction function = builder()
        .addPoint(1, Integer.valueOf(1))
        .addPoint(2, Integer.valueOf(2))
        .withLinearInterpolation()
        .build();

    assertEquals(Double.valueOf(1.5), function.call(Collections.singletonList(new BigDecimal(1.5))));
  }

  @Test
  public void canDefineAFunctionWithTwoPolynomialsAndLinearInterpolation() {
    // https://www.desmos.com/calculator/kreo2ssqj8
    DiscreteFunction function = builder()
        .addFunction(-10, -1, Maths.newPolynomial(new double[] {5, 10, 4}))
        .addFunction(1, 10, Maths.newPolynomial(new double[] {5, -10, 4}))
        .withLinearInterpolation()
        .build();

    assertEquals(-1D, function.call(Collections.singletonList(0)));
  }

  @Test
  public void canDefineTwoFunctionsAcrossAPoint() {
    DiscreteFunction function = builder()
        .addFunction(-10, 0, Maths.newPolynomial(new double[] {5, 10, 4}))
        .addFunction(0, 10, Maths.newPolynomial(new double[] {5, -10, 4}))
        .build();

    assertEquals(Double.valueOf(5), function.call(Collections.singletonList(0)));
  }

  @Test
  public void canDefineAFunctionAcrossManyPointsWithLinearInterpolation() {
    DiscreteFunction function = builder()
        .addPoint(0, 5)
        .addPoint(5, 10)
        .addPoint(10, 20)
        .withLinearInterpolation()
        .build();

    assertEquals(7D, function.call(Collections.singletonList(2)));
    assertEquals(10D, function.call(Collections.singletonList(5)));
    assertEquals(15D, function.call(Collections.singletonList(7.5D)));
    assertEquals(20D, function.call(Collections.singletonList(10)));
  }

  @Test
  public void canDefineAFunctionAcrossManySubIntegerPointsWithLinearInterpolation() {
    DiscreteFunction function = builder()
        .addPoint(0D, 0)
        .addPoint(0.1D, 1)
        .addPoint(0.3D, 3)
        .addPoint(0.5D, 4)
        .withLinearInterpolation()
        .build();

    assertEquals(0D, function.call(Collections.singletonList(0)));
    assertEquals(1D, function.call(Collections.singletonList(0.1D)));
    assertEquals(3D, function.call(Collections.singletonList(0.3D)));
    assertEquals(4D, function.call(Collections.singletonList(0.5D)));

    assertEquals(2D, function.call(Collections.singletonList(0.2D)));
    assertEquals(3.5D, function.call(Collections.singletonList(0.4D)));
  }

  @Test
  public void canDefineAFunctionWithRangesInAnyOrder() {
    DiscreteFunction function = builder()
        .addConstant(25, 50, 1000)
        .addConstant(-100, -50, -1000)
        .addConstant(5, 15, 100)
        .addConstant(-15, -5, -100)
        .build();

    assertEquals(-1000L, function.call(Collections.singletonList(-100)));
    assertEquals(-1000L, function.call(Collections.singletonList(-51)));
    assertEquals(-1000L, function.call(Collections.singletonList(-50)));

    assertEquals(-100L, function.call(Collections.singletonList(-15)));
    assertEquals(-100L, function.call(Collections.singletonList(-6)));
    assertEquals(-100L, function.call(Collections.singletonList(-5)));

    assertEquals(100L, function.call(Collections.singletonList(5)));
    assertEquals(100L, function.call(Collections.singletonList(14)));
    assertEquals(100L, function.call(Collections.singletonList(15)));

    assertEquals(1000L, function.call(Collections.singletonList(25)));
    assertEquals(1000L, function.call(Collections.singletonList(49)));
  }

  @Test
  public void canDefineAFunctionWithRangesInAnyOrderWithLinearInterpolation() {
    DiscreteFunction function = builder()
        .addConstant(25, 50, 1000)
        .addConstant(-100, -50, -1000)
        .addConstant(5, 15, 100)
        .addConstant(-15, -5, -100)
        .withLinearInterpolation()
        .build();

    assertEquals(-1000D, function.call(Collections.singletonList(-100)));
    assertEquals(-1000D, function.call(Collections.singletonList(-50)));

    assertEquals(-100D, function.call(Collections.singletonList(-15)));
    assertEquals(-100D, function.call(Collections.singletonList(-5)));

    assertEquals(0D, function.call(Collections.singletonList(0)));

    assertEquals(100D, function.call(Collections.singletonList(5)));
    assertEquals(100D, function.call(Collections.singletonList(15)));
    assertEquals(550D, function.call(Collections.singletonList(20)));

    assertEquals(1000D, function.call(Collections.singletonList(25)));
    assertEquals(1000D, function.call(Collections.singletonList(49)));
  }

  @Test
  public void canDefineAFunctionWithVaryingReturnTypes() {
    assertEquals(Types.DECIMAL, builder()
        .addPoint(0, new BigDecimal("12.455"))
        .addPoint(1, new BigDecimal("12.455"))
        .build().getReturnType());

    assertEquals(Types.INTEGER, builder()
        .addPoint(0, Integer.valueOf(1))
        .addPoint(1, Integer.valueOf(2))
        .build().getReturnType());

    assertEquals(Types.DECIMAL, builder()
        .addPoint(0, new BigDecimal("12.455"))
        .addPoint(1, Integer.valueOf(6))
        .build().getReturnType());
  }

  @Test(expected=IllegalArgumentException.class)
  public void attemptingToDefineAFunctionWithOverlappingPointsThrowsAnError() {
    builder()
        .addPoint(0, new BigDecimal("12.455"))
        .addPoint(0, new BigDecimal("12.455"));
  }

  @Test(expected=IllegalArgumentException.class)
  public void attemptingToDefineAFunctionWithOverlappingRangesThrowsAnError() {
    builder()
        .addConstant(0, 5, new BigDecimal("12.455"))
        .addConstant(3, 6, new BigDecimal("12.455"));
  }

  @Test
  public void willCoerceTheArgumentsAndTheReturnedValue() {
    RiskscapeFunction inner1 = mock(RiskscapeFunction.class);
    when(inner1.getReturnType()).thenReturn(Types.DECIMAL);
    when(inner1.getArgumentTypes()).thenReturn(Collections.singletonList(Types.INTEGER));
    when(inner1.call(eq(Collections.singletonList(Long.valueOf(1))))).thenReturn(new BigDecimal(44));

    assertEquals(new BigDecimal(44), inner1.call(Collections.singletonList(Long.valueOf(1))));

    RiskscapeFunction inner2 = mock(RiskscapeFunction.class);
    when(inner2.getReturnType()).thenReturn(Types.INTEGER);
    when(inner2.getArgumentTypes()).thenReturn(Collections.singletonList(Types.DECIMAL));
    when(inner2.call(eq(Collections.singletonList(new BigDecimal(2))))).thenReturn(Integer.valueOf(88));

    RiskscapeFunction discrete = builder().addPoint(1, inner1).addPoint(2, inner2).build();
    assertEquals(Types.DECIMAL, discrete.getReturnType());
    assertEquals(Types.DECIMAL, discrete.getArgumentTypes().get(0));

    assertEquals(new BigDecimal(44), discrete.call(Collections.singletonList("1")));
    assertEquals(new BigDecimal(88), discrete.call(Collections.singletonList("2")));
  }

  @Test(expected=IllegalArgumentException.class)
  public void willComplainIfASubFunctionDeclaresNoArguments() {
    RiskscapeFunction function = mock(RiskscapeFunction.class);
    when(function.getReturnType()).thenReturn(Types.DECIMAL);
    when(function.getArgumentTypes()).thenReturn(Collections.emptyList());

    builder().addPoint(1, function);
  }

  @Test(expected=IllegalArgumentException.class)
  public void willComplainIfASubFunctionDeclaresManyArguments() {
    RiskscapeFunction function = mock(RiskscapeFunction.class);
    when(function.getReturnType()).thenReturn(Types.DECIMAL);
    when(function.getArgumentTypes()).thenReturn(Lists.newArrayList(Types.INTEGER, Types.DECIMAL));

    builder().addPoint(1, function);
  }

  @Test
  public void callFailsIfGivenXValueIsOutsideOfRange() {
    DiscreteFunction function = builder().addPoint(1, 0).addPoint(4, 0).withLinearInterpolation().build();
    Assert.assertThrows(IllegalArgumentException.class, () -> function.call(Collections.singletonList(0)));
  }

  @Test
  public void willCloseTheUpperBoundsOfTheHighestRangeByDefault() {
    BoundType boundType = builder()
        .addConstant(0, 10, 1)
        .build()
        .getPairs().get(0).getRange().upperBoundType();

    assertEquals(
        BoundType.CLOSED,
        boundType);

    boundType = builder()
        .add(Range.open(0D, 10D), Maths.newConstant(0))
        .build()
        .getPairs().get(0).getRange().upperBoundType();

    assertEquals(
        BoundType.CLOSED,
        boundType);

    boundType = builder()
        .addPoint(0, 0)
        .addPoint(1, 0)
        .addPoint(2, 0)
        .addConstant(3, 6, 0)
        .build()
        .getPairs().get(3).getRange().upperBoundType();

    assertEquals(
        BoundType.CLOSED,
        boundType);

    // edge case - only a single, unbounded range
    boundType = builder()
        .add(Range.upTo(10D, BoundType.OPEN), Maths.newConstant(1))
        .build()
        .getPairs().get(0).getRange().upperBoundType();

    assertEquals(
        BoundType.CLOSED,
        boundType);

    // edge case - infinite range (not really a discrete function, but allowable
    assertFalse(builder()
        .add(Range.all(), Maths.newConstant(1))
        .build()
        .getPairs().get(0).getRange().hasUpperBound());
  }

  @Test
  public void willCloseTheUpperBoundsOfAnyDisconnectedRanges() {
    DiscreteFunction function = builder()
        .addConstant(0, 10, 1)
        .addConstant(11, 20, 1)
        .build();

    assertEquals(BoundType.CLOSED, function.getPairs().get(0).getRange().upperBoundType());
    assertEquals(BoundType.CLOSED, function.getPairs().get(1).getRange().upperBoundType());
  }

  @Test
  public void canSuppressTheClosingOfTheUpperBoundsOfTheHighestRange() {
    BoundType boundType = builder()
        .addConstant(0, 10, 1)
        .withoutUpperBoundClosing()
        .build()
        .getPairs().get(0).getRange().upperBoundType();

    assertEquals(
        BoundType.OPEN,
        boundType);
  }

  @Test
  public void linearInterpolationWillCloseAcrossAnyRange() {
    DiscreteFunction function = builder()
        .addConstant(-10, -1, 1)
        .addConstant(1, 10, 1)
        .withLinearInterpolation()
        .build();

    assertEquals(1D, function.call(Collections.singletonList(0)));
    assertEquals(1D, function.call(Collections.singletonList(-1)));
    assertEquals(1D, function.call(Collections.singletonList(1)));
  }

  @Test
  public void canHaveAFunctionWithInfiniteBounds() {
    DiscreteFunction function = builder()
        .add(Range.upTo(-10D, BoundType.OPEN), Maths.newConstant(0))
        .addPoint(0, 1)
        .add(Range.downTo(10D, BoundType.CLOSED), Maths.newConstant(2))
        .withLinearInterpolation()
        .build();

    assertEquals(0D, function.call(Collections.singletonList(-1000)));
    assertEquals(2D, function.call(Collections.singletonList(1000)));
    assertEquals(1D, function.call(Collections.singletonList(0)));
    assertEquals(1.1D, function.call(Collections.singletonList(1)));
    assertEquals(0.9D, function.call(Collections.singletonList(-1)));
  }

}
