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

import static org.junit.Assert.*;

import org.geotools.referencing.factory.epsg.CartesianAuthorityFactory;
import org.junit.Test;

import nz.org.riskscape.engine.types.CoverageType;
import nz.org.riskscape.engine.types.EmptyList;
import nz.org.riskscape.engine.types.MultiGeom;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.Referenced;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.TypeRegistry;
import nz.org.riskscape.engine.types.TypeRulesTest;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.types.WithinRange;
import nz.org.riskscape.engine.types.WithinSet;

public class DefaultVarianceRulesTest extends TypeRulesTest {

  @Override
  protected void addTypes() {
    TypeRegistry.addDefaultVarianceRules(registry);
  }

  @Test
  public void testAnything() throws Exception {
    assertVariance(Variance.COVARIANT, Types.TEXT, Types.ANYTHING);
    assertVariance(Variance.INVARIANT, Types.ANYTHING, Types.TEXT);
    assertVariance(Variance.COVARIANT, RSList.create(Types.INTEGER), Types.ANYTHING);
    // note that you still can't pass nothing/null where target is Anything
    // (target would have to be nullable(Anything))
    assertVariance(Variance.INVARIANT, Types.NOTHING, Types.ANYTHING);
    assertVariance(Variance.INVARIANT, Nullable.TEXT, Types.ANYTHING);
    assertVariance(Variance.INVARIANT, Nullable.ANYTHING, Types.ANYTHING);
  }

  @Test
  public void testListAnything() throws Exception {
    Type anythingList = RSList.create(Types.ANYTHING);
    Type textList = RSList.create(Types.TEXT);
    Type structList = RSList.create(Struct.of("foo", Types.FLOATING));

    // basic happy cases where target is List[Anything]
    assertVariance(Variance.COVARIANT, textList, anythingList);
    assertVariance(Variance.COVARIANT, structList, anythingList);
    assertVariance(Variance.EQUAL, RSList.create(Types.ANYTHING), anythingList);

    // rule doesn't work the other way, i.e. List[Anything] --> List[Concrete-thing]
    assertVariance(Variance.INVARIANT, anythingList, Nullable.of(structList));
    assertVariance(Variance.INVARIANT, anythingList, textList);
    assertVariance(Variance.INVARIANT, Nullable.of(anythingList), textList);

    // and obviously a single item can't be accepted as a list
    assertVariance(Variance.INVARIANT, Types.ANYTHING, anythingList);
    assertVariance(Variance.INVARIANT, Types.TEXT, textList);
    assertVariance(Variance.INVARIANT, Struct.of("foo", Types.FLOATING), structList);
  }

  @Test
  public void testEmptyLists() throws Exception {
    Type anythingList = RSList.create(Types.ANYTHING);
    Type textList = RSList.create(Types.TEXT);
    Type structList = RSList.create(Struct.of("foo", Types.FLOATING));
    Type emptyList = EmptyList.INSTANCE;

    // an empty list can be assigned to any other list type
    assertVariance(Variance.COVARIANT, emptyList, anythingList);
    assertVariance(Variance.COVARIANT, emptyList, structList);
    assertVariance(Variance.COVARIANT, emptyList, textList);

    // but the converse isn't true - if we want an empty list (in practice I can't see why we would) then that's what
    // it has to be
    assertVariance(Variance.INVARIANT, anythingList, emptyList);
    assertVariance(Variance.INVARIANT, structList, emptyList);
    assertVariance(Variance.INVARIANT, textList, emptyList);
  }

  @Test
  public void testNothing() throws Exception {
    assertVariance(Variance.EQUAL, Types.NOTHING, Types.NOTHING);
    assertVariance(Variance.COVARIANT, Types.NOTHING, Nullable.TEXT);
    assertVariance(Variance.INVARIANT, Types.NOTHING, Types.TEXT);
    // java rule says we can't assign text to void
    assertVariance(Variance.INVARIANT, Types.TEXT, Types.NOTHING);
  }

  @Test
  public void testNullable() throws Exception {
    assertVariance(Variance.EQUAL, Nullable.TEXT, Nullable.TEXT);
    assertVariance(Variance.EQUAL, Types.TEXT, Nullable.TEXT);
    assertVariance(Variance.COVARIANT, Nullable.TEXT, Nullable.ANYTHING);
    assertVariance(Variance.COVARIANT, Types.TEXT, Nullable.ANYTHING);
    assertVariance(Variance.INVARIANT, Types.ANYTHING, Nullable.TEXT);
    assertVariance(Variance.INVARIANT, Nullable.TEXT, Types.TEXT);
    assertVariance(Variance.INVARIANT, Nullable.ANYTHING, Nullable.TEXT);
  }

  @Test
  public void testListNullable() throws Exception {
    Type anythingList = RSList.create(Types.ANYTHING);
    Type floatList = RSList.create(Types.FLOATING);

    // basic happy cases where target is nullable(List[Anything])
    assertVariance(Variance.COVARIANT, floatList, Nullable.of(anythingList));
    assertVariance(Variance.COVARIANT, Nullable.of(floatList), Nullable.of(anythingList));
    assertVariance(Variance.COVARIANT, Types.NOTHING, Nullable.of(anythingList));
    assertVariance(Variance.EQUAL, anythingList, Nullable.of(anythingList));

    // still can't pass a nullable list where null is not expected/handled
    assertVariance(Variance.INVARIANT, Nullable.of(floatList), anythingList);
    assertVariance(Variance.INVARIANT, Nullable.of(anythingList), anythingList);

    // also check the case where the items in the list are themselves nullable
    Type listOfNullableAnything = RSList.create(Nullable.of(Types.ANYTHING));
    Type listOfNullableFloats = RSList.create(Nullable.of(Types.FLOATING));
    assertVariance(Variance.COVARIANT, floatList, listOfNullableAnything);
    assertVariance(Variance.COVARIANT, listOfNullableFloats, listOfNullableAnything);
    assertVariance(Variance.EQUAL, floatList, listOfNullableFloats);

    // but can't pass nullable items to something that's not expecting them
    assertVariance(Variance.INVARIANT, listOfNullableFloats, floatList);
    assertVariance(Variance.INVARIANT, listOfNullableAnything, anythingList);
    assertVariance(Variance.INVARIANT, Nullable.of(anythingList), listOfNullableAnything);
  }

  @Test
  public void testGeometrySubtypes() {
    assertVariance(Variance.COVARIANT, Types.LINE, Types.GEOMETRY);
    assertVariance(Variance.COVARIANT, MultiGeom.MULTI_LINE, Types.GEOMETRY);
    assertVariance(Variance.COVARIANT, Types.POINT, Types.GEOMETRY);
    assertVariance(Variance.COVARIANT, MultiGeom.MULTI_POINT, Types.GEOMETRY);
    assertVariance(Variance.COVARIANT, Types.POLYGON, Types.GEOMETRY);
    assertVariance(Variance.COVARIANT, MultiGeom.MULTI_POLYGON, Types.GEOMETRY);
  }

  @Test
  public void testCoverageTypes() {
    assertVariance(Variance.COVARIANT, new CoverageType(Types.INTEGER), CoverageType.WILD);

    // when source is wrapped in a referenced
    assertVariance(
        Variance.COVARIANT,
        Referenced.of(new CoverageType(Types.INTEGER), CartesianAuthorityFactory.GENERIC_2D),
        CoverageType.WILD
    );
  }

  @Test
  public void textMixedContainingTypes() throws Exception {
    // nope - same members doesn't mean they can be assigned to each other
    assertVariance(Variance.INVARIANT, RSList.create(Types.TEXT), new CoverageType(Types.TEXT));
    // but these can...
    assertVariance(Variance.EQUAL, RSList.create(Types.TEXT), RSList.create(Types.TEXT));
  }

  @Test
  public void testWrappingTypes() throws Exception {
    assertVariance(Variance.EQUAL,
        new WithinSet(Types.TEXT, "foo", "bar"),
        new WithinSet(Types.TEXT, "foo", "bar")
    );

    assertVariance(Variance.COVARIANT,
        new WithinSet(Types.TEXT, "foo", "bar"),
        Types.TEXT
    );

    assertVariance(Variance.INVARIANT,
        Types.TEXT,
        new WithinSet(Types.TEXT, "foo", "bar")
   );

    assertVariance(Variance.UNKNOWN,
        new WithinRange(Types.INTEGER, 0, 10),
        new WithinSet(Types.INTEGER, 5L, 10L)
    );

    assertVariance(Variance.UNKNOWN,
        new WithinRange(Types.INTEGER, 0, 10),
        new WithinRange(Types.INTEGER, 5L, 10L)
    );

  }

  @Test
  public void testLinkedTypes() throws Exception {
    typeSet.add("foo", Types.TEXT);
    typeSet.add("bar", Types.TEXT);
    typeSet.add("baz", new WithinSet(Types.TEXT, "foo", "bar"));

    assertVariance(Variance.EQUAL, typeSet.get("foo"), typeSet.get("bar"));
    assertVariance(Variance.EQUAL, typeSet.get("foo"), Types.TEXT);
    assertVariance(Variance.EQUAL, Types.TEXT, typeSet.get("foo"));

    assertVariance(Variance.COVARIANT, typeSet.get("baz"), typeSet.get("bar"));
    assertVariance(Variance.INVARIANT, typeSet.get("foo"), typeSet.get("baz"));
  }

  @Test
  public void testStructs() throws Exception {
    // because of an overly-conservative decision w.r.t to struct access via members, we can not interchange
    // tuples from different types, even if they are Object#equals - so we must say they are invariant
    // We could drop this check as long as it was well considered and backed up with some other checks elsewhere,
    // as it often catches type screw ups
    assertVariance(
      Variance.UNKNOWN,
      Struct.of("foo", Types.TEXT),
      Struct.of("bar", Types.TEXT)
    );

    assertVariance(
      Variance.UNKNOWN,
      Struct.of("foo", Types.TEXT, "bar", Types.TEXT),
      Struct.of("foo", Types.TEXT)
    );

    Struct foo = Struct.of("foo", Types.TEXT);
    assertVariance(
      Variance.EQUAL,
      foo,
      foo
    );
  }

  @Test
  public void testNullableStructs() throws Exception {
    assertVariance(
      Variance.INVARIANT,
      Nullable.of(Struct.of("foo", Types.TEXT)),
      Struct.of("foo", Types.TEXT)
    );

    assertVariance(
      Variance.UNKNOWN,
      Struct.of("foo", Types.TEXT),
      Nullable.of(Struct.of("foo", Types.TEXT))
    );

    assertVariance(
      Variance.UNKNOWN,
      Nullable.of(Struct.of("foo", Types.TEXT, "bar", Types.TEXT)),
      Nullable.of(Struct.of("foo", Types.TEXT))
    );

    Struct structType = Struct.of("foo", Types.TEXT);
    assertVariance(Variance.EQUAL, Nullable.of(structType), Nullable.of(structType));
    assertVariance(Variance.EQUAL, structType, Nullable.of(structType));
    assertVariance(Variance.INVARIANT, Nullable.of(structType), structType);
  }

  @Test
  public void nullableLinkedStructsDoNotNeedToBeCoerced() throws Exception {
    // we had a case where we compared linked(struct(foo: bar)) to struct(foo: bar) where
    // the structs were the same object, and it ended up being invariant because of a bunch of reasons
    // (1 - struct equals skipping was giving invariant instead of unknown) and

    Struct struct = Struct.of("foo", Types.TEXT);
    Type linkedType = typeSet.add("footype", struct);

    assertVariance(Variance.EQUAL, linkedType, struct);
    assertNull(typeSet.findEquivalenceCoercer(struct, linkedType).orElse(null));
  }

  private void assertVariance(Variance expectedVariance, Type sourceType, Type targetType) {
    assertEquals("Expected " + sourceType + " to be " + expectedVariance + " to " + targetType,
      expectedVariance,
      typeSet.testVariance(sourceType, targetType)
    );
  }

}
