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

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.util.Arrays;

import org.geotools.referencing.CRS;
import org.junit.Test;

import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.SRIDSet;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.gt.GeometryHelper;
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.Types;
import nz.org.riskscape.engine.types.WithinRange;
import nz.org.riskscape.engine.typeset.TypeSet;
import nz.org.riskscape.rl.ExpressionParser;
import nz.org.riskscape.rl.ast.StructDeclaration;

public class UnionProjectorTest extends ProjectTest {

  Projector projector;
  GeometryHelper nzGeomHelper = GeometryHelper.nzTransverseMercatorWithOriginShift(project.getSridSet());
  TypeSet typeSet = new TypeSet();

  @Test
  public void canTakeUnionOfTwoIdenticalStructs() throws Exception {
    assertThat(union(Struct.EMPTY_STRUCT, Struct.EMPTY_STRUCT), is(Struct.EMPTY_STRUCT));

    assertThat(union(Struct.of("foo", Types.INTEGER), Struct.of("foo", Types.INTEGER)),
        is(Struct.of("foo", Types.INTEGER)));

    Struct testStruct = Struct.of("foo", Nullable.FLOATING, "bar", MultiGeom.MULTI_POLYGON);
    Struct duplicate = Struct.of("foo", Nullable.FLOATING, "bar", MultiGeom.MULTI_POLYGON);
    assertThat(union(testStruct, duplicate), is(testStruct));
  }

  @Test
  public void sameAttributesButDifferentOrder() throws Exception {
    Struct struct = Struct.of("foo", Nullable.FLOATING, "bar", Types.POLYGON, "baz", Types.BOOLEAN);
    Struct orderFlipped = Struct.of("baz", Types.BOOLEAN, "bar", Types.POLYGON, "foo", Nullable.FLOATING);

    // use the attribute order of first struct arg
    assertThat(union(orderFlipped, struct), is(orderFlipped));
    assertThat(union(struct, orderFlipped), is(struct));

    Tuple orderedTuple = Tuple.ofValues(struct, 0.123D, nzGeomHelper.emptyBox(), true);
    Tuple flippedTuple = Tuple.ofValues(orderFlipped, false, nzGeomHelper.emptyBox(), 1.23D);
    assertThat(projector.apply(orderedTuple), is(orderedTuple));
    assertThat(projector.apply(flippedTuple), is(Tuple.ofValues(struct, 1.23D, nzGeomHelper.emptyBox(), false)));
  }

  @Test
  public void commonTypeUsedIfTypesDiffer() throws Exception {
    Struct struct1 = Struct.of("float", Nullable.FLOATING, "geom", Types.POLYGON, "number", Types.INTEGER);
    Struct struct2 = Struct.of("float", Types.FLOATING, "geom", Types.POINT, "number", Nullable.FLOATING);
    Struct expected = Struct.of("float", Nullable.FLOATING, "geom", Types.GEOMETRY, "number", Nullable.ANYTHING);

    assertThat(union(struct1, struct2), is(expected));
    assertThat(union(struct2, struct1), is(expected));

    assertThat(project(struct1, null, nzGeomHelper.emptyBox(), 123L),
        is(Tuple.ofValues(expected, null, nzGeomHelper.emptyBox(), 123L)));
    assertThat(project(struct1, 0.123D, nzGeomHelper.emptyPoint(), null),
        is(Tuple.ofValues(expected, 0.123D, nzGeomHelper.emptyPoint(), null)));
  }

  @Test
  public void anyMissingAttributesAreNullable() throws Exception {
    Struct nested = Struct.of("qux", Types.TEXT);
    Struct struct1 = Struct.of("foo", Types.FLOATING, "bar", nested);
    Struct struct2 = Struct.of("foo", Types.FLOATING, "baz", Referenced.of(Types.POINT, nzGeomHelper.getCrs()));
    Struct expected = Struct.of("foo", Types.FLOATING, "bar", Nullable.of(nested), "baz",
        Nullable.of(Referenced.of(Types.POINT, nzGeomHelper.getCrs())));

    assertThat(union(struct1, struct2), is(expected));

    Tuple nestedValue = Tuple.ofValues(nested, "cool story");
    assertThat(project(struct1, 0.123D, nestedValue),
        is(Tuple.ofValues(expected, 0.123D, nestedValue, null)));
    assertThat(project(struct2, 4.56D, nzGeomHelper.emptyPoint()),
        is(Tuple.ofValues(expected, 4.56D, null, nzGeomHelper.emptyPoint())));
  }

  @Test
  public void nullableTypesArePreserved() throws Exception {
    // don't double-null things
    Struct struct1 = Struct.of("foo", Nullable.FLOATING);
    Struct struct2 = Struct.of("bar", Nullable.TEXT);
    Struct expected = Struct.of("foo", Nullable.FLOATING, "bar", Nullable.TEXT);

    assertThat(union(struct1, struct2), is(expected));

    assertThat(projector.apply(Tuple.of(struct1, "foo", null)),
        is(Tuple.ofValues(expected, null, null)));
    assertThat(projector.apply(Tuple.of(struct2, "bar", null)),
        is(Tuple.ofValues(expected, null, null)));
  }

  @Test
  public void nullabilityOfNestedStructIsPreserved() throws Exception {
    Struct nested = Struct.of("bar", Nullable.TEXT);
    Struct struct1 = Struct.of("foo", nested);
    Struct struct2 = Struct.of("foo", Nullable.of(nested));
    Struct expected = Struct.of("foo", Nullable.of(nested));

    assertThat(union(struct1, struct2), is(expected));

    assertThat(project(struct1, Tuple.of(nested, "bar", null)),
        is(Tuple.ofValues(expected, Tuple.of(nested, "bar", null))));
    assertThat(projector.apply(Tuple.of(struct1, "foo", null)),
        is(Tuple.of(expected, "foo", null)));
  }

  @Test
  public void canTakeUnionOfNestedStructs() throws Exception {
    // this is important because it's how we deal with the exposure-layer
    Struct nested1 = Struct.of("foo", RSList.create(Types.FLOATING), "bar", Types.INTEGER, "baz", Types.TEXT);
    Struct struct1 = Struct.of("exposure", nested1);
    Struct nested2 = Struct.of("bar", Types.INTEGER, "foo", RSList.create(Types.INTEGER), "qux", Types.BOOLEAN);
    Struct struct2 = Struct.of("exposure", nested2);
    // TODO foo should really be RSList.create(Types.ANYTHING) here, but this is a
    // general problem with types. See GL827 for more details
    Struct expectedNested = Struct.of("foo", Types.ANYTHING, "bar", Types.INTEGER, "baz", Nullable.TEXT,
        "qux", Nullable.BOOLEAN);
    Struct expected = Struct.of("exposure", expectedNested);

    assertThat(union(struct1, struct2), is(expected));

    // need to recurse the struct when projecting and make sure we null everything
    assertThat(project(struct1, Tuple.ofValues(nested1, Arrays.asList(1.0D, 2.0D, 3.0D), 123L, "bazza")),
        is(Tuple.ofValues(expected,
            Tuple.ofValues(expectedNested, Arrays.asList(1.0D, 2.0D, 3.0D), 123L, "bazza", null))
    ));
    assertThat(project(struct2, Tuple.ofValues(nested2, 789L, Arrays.asList(4L, 5L, 6L), true)),
        is(Tuple.ofValues(expected,
            Tuple.ofValues(expectedNested, Arrays.asList(4L, 5L, 6L), 789L, null, true))
    ));
  }

  @Test
  public void canTakeUnionOfMoreThanTwoStructs() throws Exception {
    Struct struct1 = Struct.of("foo", Types.FLOATING, "bar", Types.INTEGER);
    Struct struct2 = Struct.of("foo", Types.FLOATING, "baz", Types.POINT);
    Struct struct3 = Struct.of("foo", Types.FLOATING, "baz", Types.POLYGON, "qux", Nullable.TEXT);
    Struct expected = Struct.of("foo", Types.FLOATING, "bar", Nullable.INTEGER, "baz", Nullable.GEOMETRY, "qux",
        Nullable.TEXT);

    projector = UnionProjector.realize(typeSet, struct1, struct2, struct3).get();
    assertThat(projector.getProjectedType(), is(expected));

    assertThat(project(struct1, 1.23D, 456L),
        is(Tuple.ofValues(expected, 1.23D, 456L, null, null)));
    assertThat(project(struct2, 4.56D, nzGeomHelper.emptyPoint()),
        is(Tuple.ofValues(expected, 4.56D, null, nzGeomHelper.emptyPoint(), null)));
    assertThat(project(struct3, 7.89D, nzGeomHelper.emptyBox(), "what is a qux anyway?"),
        is(Tuple.ofValues(expected, 7.89D, null, nzGeomHelper.emptyBox(), "what is a qux anyway?")));
  }

  @Test
  public void wrappedTypesArePreservedIfEqual() throws Exception {
    Struct struct1 = Struct.of("range", new WithinRange(Types.INTEGER, 1L, 10L), "geom",
        Referenced.of(Types.POINT, nzGeomHelper.getCrs()));
    Struct struct2 = Struct.of("range", new WithinRange(Types.INTEGER, 1L, 10L), "geom",
        Referenced.of(Types.POINT, nzGeomHelper.getCrs()));

    // structs are the same, so projection shouldn't change anything
    assertThat(union(struct1, struct2), is(struct1));

    assertThat(project(struct1, 5L, nzGeomHelper.emptyPoint()),
        is(Tuple.ofValues(struct1, 5L, nzGeomHelper.emptyPoint())));
  }

  @Test
  public void wrappedTypesAreDroppedIfNotEqual() throws Exception {
    Struct struct1 = Struct.of("range", new WithinRange(Types.INTEGER, 1L, 10L),
        "geom1", Referenced.of(Types.POINT, nzGeomHelper.getCrs()),
        "geom2", Referenced.of(Types.POLYGON, nzGeomHelper.getCrs()));
    Struct struct2 = Struct.of("range", new WithinRange(Types.INTEGER, 1L, 9L),
        "geom1", Referenced.of(Types.POINT, SRIDSet.EPSG4326_LONLAT),
        "geom2", Referenced.of(Types.POINT, nzGeomHelper.getCrs()));
    // geom1 = same type, different Referenced, geom2 = same Referenced, different type
    Struct expected = Struct.of("range", Types.INTEGER,
        "geom1", Types.POINT,
        "geom2", Referenced.of(Types.GEOMETRY, nzGeomHelper.getCrs()));

    assertThat(union(struct1, struct2), is(expected));

    assertThat(project(struct1, 5L, nzGeomHelper.emptyPoint(), nzGeomHelper.emptyBox()),
        is(Tuple.ofValues(expected, 5L, nzGeomHelper.emptyPoint(), nzGeomHelper.emptyBox())));
  }

  @Test
  public void referencedTypesAreNotPreservedIfCrsDiffers() throws Exception {
    // same CRS but the axis is flipped
    Struct struct1 = Struct.of("foo", Referenced.of(Types.POLYGON, CRS.decode("EPSG:4326", false)));
    Struct struct2 = Struct.of("foo", Referenced.of(Types.POLYGON, CRS.decode("EPSG:4326", true)));
    Struct expected = Struct.of("foo", Types.POLYGON);
    assertThat(union(struct1, struct2), is(expected));
  }

  @Test
  public void wrappedTypesArePreservedIfOnlyOnePresent() throws Exception {
    Type range = new WithinRange(Types.INTEGER, 1L, 10L);
    Type referenced = Referenced.of(Types.POLYGON, SRIDSet.EPSG4326_LONLAT);
    Struct struct1 = Struct.of("foo", referenced);
    Struct struct2 = Struct.of("bar", range);
    Struct expected = Struct.of("foo", Nullable.of(referenced), "bar", Nullable.of(range));
    assertThat(union(struct1, struct2), is(expected));
  }

  private Tuple project(Struct struct, Object... values) {
    return projector.apply(Tuple.ofValues(struct, values));
  }

  private Struct union(Struct a, Struct b) {
    Struct result = UnionProjector.unionOf(typeSet, a, b);
    projector = UnionProjector.realize(typeSet, a, b).get();
    return result;
  }

  StructDeclaration toStructDeclaration(String source) {
    return ExpressionParser.parseString(source).isA(StructDeclaration.class).get();
  }

}
