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

import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.junit.Test;

import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.ResultOrProblems;

public class JavaParameterSetTest {

  BindingContext context = mock(BindingContext.class);

  public static class VanillaFields {
    @ParameterField
    public Integer fooInteger;

    @ParameterField
    public long barLong;

    @ParameterField
    public String bazString = "this is the default for bar";
  }

  @Test
  public void canWrapAndBindToSomeSimpleFields() throws Exception {
    JavaParameterSet<VanillaFields> vanillaFieldSet = JavaParameterSet.fromBindingClass(VanillaFields.class);
    assertThat(
      vanillaFieldSet.getDeclared(),
      contains(
          Parameter.required("foo-integer", Integer.class),
          Parameter.required("bar-long", Long.class),
          Parameter.required("baz-string", String.class)
      )
    );

    Map<String, List<?>> valueMap = new HashMap<>();
    valueMap.put("foo-integer", Arrays.asList(1));
    valueMap.put("bar-long", Arrays.asList(2L));
    valueMap.put("baz-string", Arrays.asList("cool"));
    BoundParameters parameters =
        new BoundParameters(vanillaFieldSet, context, valueMap, Collections.emptyMap(), Collections.emptyList());

    VanillaFields fields = vanillaFieldSet.bindToObject(parameters).getBoundToObject();
    assertEquals(Integer.valueOf(1), fields.fooInteger);
    assertEquals(2L, fields.barLong);
    assertEquals("cool", fields.bazString);
  }

  public static class ContainerFields {
    @ParameterField
    public Optional<Integer> fooInteger;

    @ParameterField
    public List<String> strings;
  }

  @Test
  public void canWrapAndBindToSomeContainerFields() throws Exception {
    JavaParameterSet<ContainerFields> containerFieldSet = JavaParameterSet.fromBindingClass(ContainerFields.class);
    assertThat(
      containerFieldSet.getDeclared(),
      contains(
          Parameter.optional("foo-integer", Integer.class),
          Parameter.range("strings", String.class, 0, Integer.MAX_VALUE)
      )
    );

    Map<String, List<?>> valueMap = new HashMap<>();
    BoundParameters parameters =
        new BoundParameters(containerFieldSet, context, valueMap, Collections.emptyMap(), Collections.emptyList());

    ContainerFields empty = containerFieldSet.bindToObject(parameters).getBoundToObject();
    assertFalse(empty.fooInteger.isPresent());
    assertTrue(empty.strings.isEmpty());

    valueMap.put("foo-integer", Arrays.asList(1));
    valueMap.put("strings", Arrays.asList("1", "2", "3"));

    ContainerFields set = containerFieldSet.bindToObject(parameters).getBoundToObject();
    assertEquals(Integer.valueOf(1), set.fooInteger.orElse(0));
    assertEquals(Arrays.asList("1", "2", "3"), set.strings);
  }

  public static class WithDefaults {
    @ParameterField
    public Optional<Integer> fooInteger = Optional.of(1);

    @ParameterField
    public long someLong = 67L;

    @ParameterField
    public String aString = "great!";

    @ParameterField
    public List<String> moreStrings = Arrays.asList("great");

    @ParameterField(defaultValue = "23")
    public Integer anotherInt;
  }

  @Test
  public void defaultsCanBeProvidedViaInstanceDefaults() throws Exception {
    JavaParameterSet<WithDefaults> withDefaults = JavaParameterSet.fromBindingClass(WithDefaults.class);

    assertEquals(Arrays.asList(1), withDefaults.get("foo-integer").getDefaultValues(context));
    assertEquals(Arrays.asList(67L), withDefaults.get("some-long").getDefaultValues(context));
    assertEquals(Arrays.asList("great!"), withDefaults.get("a-string").getDefaultValues(context));
    assertEquals(Arrays.asList("great"), withDefaults.get("more-strings").getDefaultValues(context));

    // this one requires the context as it's given as a string
    when(context.bind(any(Parameter.class), eq("23"))).thenReturn(24);
    assertEquals(Arrays.asList(24), withDefaults.get("another-int").getDefaultValues(context));
  }



  @Test
  public void defaultsCanBeProvidedViaCustomizedDefaults() throws Exception {
    WithDefaults instance = new WithDefaults();
    instance.anotherInt = 55; // this gets ignored - see note below :(
    instance.fooInteger = Optional.empty();
    instance.moreStrings = Arrays.asList("one", "two", "three");
    instance.someLong = 420L;
    instance.aString = null;

    JavaParameterSet<WithDefaults> withDefaults = JavaParameterSet.fromBindingInstance(WithDefaults.class, instance);

    assertEquals(Arrays.asList(), withDefaults.get("foo-integer").getDefaultValues(context));
    assertEquals(Arrays.asList(420L), withDefaults.get("some-long").getDefaultValues(context));
    assertEquals(Arrays.asList(), withDefaults.get("a-string").getDefaultValues(context));
    assertEquals(Arrays.asList("one", "two", "three"), withDefaults.get("more-strings").getDefaultValues(context));

    // XXX Note that this is possibly not what we want here - even though the supplied instance gives a value, it is
    // ignored in favour of the annotated value.  We probably could say the instance always wins, but that's a change I
    // don't want to make right now - we can probably do it once it's actually needed.
    // this one requires the context as it's given as a string
    when(context.bind(any(Parameter.class), eq("23"))).thenReturn(24);
    assertEquals(Arrays.asList(24), withDefaults.get("another-int").getDefaultValues(context));
  }

  public static class Embedded {
    @IncludeParameters
    public VanillaFields vanillaFields = new VanillaFields();
  }

  @Test
  public void aParameterClassCanEmbedAnotherParameterClass() throws Exception {
    JavaParameterSet<Embedded> embedded = JavaParameterSet.fromBindingClass(Embedded.class);

    assertThat(
        embedded.getDeclared(),
        contains(
            Parameter.required("foo-integer", Integer.class),
            Parameter.required("bar-long", Long.class),
            Parameter.required("baz-string", String.class)
        )
      );

      Map<String, List<?>> valueMap = new HashMap<>();
      valueMap.put("foo-integer", Arrays.asList(1));
      valueMap.put("bar-long", Arrays.asList(2L));
      valueMap.put("baz-string", Arrays.asList("cool"));
      BoundParameters parameters =
        new BoundParameters(embedded, context, valueMap, Collections.emptyMap(), Collections.emptyList());

      VanillaFields fields = embedded.bindToObject(parameters).getBoundToObject().vanillaFields;
      assertEquals(Integer.valueOf(1), fields.fooInteger);
      assertEquals(2L, fields.barLong);
      assertEquals("cool", fields.bazString);
  }

  @Test
  public void testFailedBindingBehaviour() throws Exception {
    JavaParameterSet<WithDefaults> parameterSet = JavaParameterSet.fromBindingClass(WithDefaults.class);

    // get all binding to fail
    when(context.bind(any(), any(Parameter.class)))
      .thenReturn(ResultOrProblems.failed(Problem.error("ruh roh")));

    Map<String, List<?>> valueMap = new HashMap<>();
    valueMap.put("foo-integer", Arrays.asList("ignored"));
    valueMap.put("some-long", Arrays.asList("ignored"));
    valueMap.put("a-string", Arrays.asList(new Object()));
    valueMap.put("more-strings", Arrays.asList(new Object()));
    valueMap.put("another-int", Arrays.asList("ignored"));


    BoundParameters bound = parameterSet.bind(context, valueMap);
    WithDefaults withDefaults = parameterSet.bindToObject(bound).getBoundToObject();

    assertEquals(withDefaults.moreStrings, Arrays.asList()); // empty list for collections
    assertEquals(withDefaults.fooInteger, Optional.empty()); // empty instead of default
    assertEquals(withDefaults.someLong, 0L); // primitive value - default is set
    assertEquals(withDefaults.aString, null); // strings (and other objects) get null
    assertEquals(withDefaults.anotherInt, null); // wrapper types get null
  }

  @Test
  public void canGetArityErrorsBindingToFields() throws Exception {
    JavaParameterSet<VanillaFields> vanillaFieldSet = JavaParameterSet.fromBindingClass(VanillaFields.class);
    Parameter fooParam = vanillaFieldSet.getDeclared().iterator().next();

    Map<String, List<?>> valueMap = new HashMap<>();
    // required foo param is missing
    valueMap.put("bar-long", Arrays.asList(2L));
    // multiple values specified for single-value baz parameter
    valueMap.put("baz-string", Arrays.asList("cool", "story", "bro"));

    BoundParameters bound = vanillaFieldSet.bind(context, valueMap);
    assertThat(bound.getValidationProblems(), contains(
        is(GeneralProblems.get().required(fooParam)),
        is(ParamProblems.get().wrongNumberGiven("baz-string", "1", 3))
    ));

    // can be naughty and still build the object without exploding
    VanillaFields vanillaFields = vanillaFieldSet.bindToObject(bound).getBoundToObject();
    assertThat(vanillaFields.fooInteger, nullValue());
    assertThat(vanillaFields.barLong, is(2L));
    // not ideal, but just default to whatever was specified last
    assertThat(vanillaFields.bazString, is("bro"));
  }
}
