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

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

import java.util.List;

import org.junit.Test;

import nz.org.riskscape.engine.cli.ExitException;
import nz.org.riskscape.engine.data.InputDataProblems;
import nz.org.riskscape.engine.test.EngineTestPlugins;

/**
 * Covers off simple cases of loading relational data into a list
 */
@EngineTestPlugins({ "defaults", "beta" })
public class RelationToListPipelineTest extends BaseModelRunCommandTest {

  String pipeline;
  List<List<String>> rows;

  @Test
  public void canLoadRelationalDataViaList() throws Exception {
    pipeline = ""
        + "input('combined.csv') "
        + "-> "
        + "select({*, to_list(bookmark(filename, {}, type: 'relation(struct(val: text))')) as value})"
        + "->"
        + "unnest(value)"
        + "->"
        + "group(by: filename, select: { *, count(*) })";

    evaluate();

    rows = openCsv("group.csv", "filename", "count");

    assertThat(
      rows,
      containsInAnyOrder(
        contains("values1-100.csv", "100"),
        contains("values101-1000.csv", "900"),
        contains("values1001-10000.csv", "9000")
      )
    );
  }

  @Test
  public void canUseRelationInListFunctions() throws Exception {
    // can use list functions like length() on RelationBackedList without unnesting
    pipeline = ""
        + "input('combined.csv') "
        + "-> "
        + "select({*, to_list(bookmark('numeric',"
        + "                            { location: filename },"
        + "                            type: 'relation(struct(val: integer))')"
        + "                   ) as list})"
        + "->"
        // note we're doing struct aggregation here
        + "select({ filename, length(list) as len, max(list) as max, mean(list) as avg }) as results";

    evaluate();

    rows = openCsv("results.csv", "filename", "len", "max.val", "avg.val");

    assertThat(
      rows,
      containsInAnyOrder(
        contains("values1-100.csv", "100", "100", "50.5"),
        contains("values101-1000.csv", "900", "1000", "550.5"),
        contains("values1001-10000.csv", "9000", "10000", "5500.5")
      )
    );
  }

  @Test
  public void canMapRelationBackedListWithoutExploding() throws Exception {
    pipeline = ""
        + "input('combined.csv') "
        + "-> "
        + "select({*, to_list(bookmark('numeric',"
        + "                            { location: filename },"
        + "                            type: 'relation(struct(val: integer))')"
        + "                   ) as list})"
        + "->"
        + "select({ filename, map(list, x -> x.val * 2) as doubled })"
        + "->"
        + "select({ filename, max(doubled) as max, mean(doubled) as avg }) as mapped";

    evaluate();

    rows = openCsv("mapped.csv", "filename", "max", "avg");

    assertThat(
      rows,
      containsInAnyOrder(
        contains("values1-100.csv", "200", "101.0"),
        contains("values101-1000.csv", "2000", "1101.0"),
        contains("values1001-10000.csv", "20000", "11001.0")
      )
    );
  }

  @Test
  public void canModifyRelationBackedListWithoutExploding() throws Exception {
    // check append() + concat() which produce new lists
    pipeline = ""
        + "input('combined.csv') "
        + "-> "
        + "select({*, to_list(bookmark('numeric',"
        + "                            { location: filename },"
        + "                            type: 'relation(struct(val: integer))')"
        + "                   ) as list})"
        + "->"
        + "select({ filename, append(list, { val: 10001 }) as appended,"
        + "         concat(list, [ {val: 20000 }]) as concatted })"
        + "->"
        + "select({ filename, "
        + "         length(appended) as append_len, max(appended) as append_max, round(mean(appended)) as append_avg,"
        + "         length(concatted) as concat_len, max(concatted) as concat_max, round(mean(concatted)) as concat_avg"
        + "}) as modified";

    evaluate();

    rows = openCsv("modified.csv", "filename", "append_len", "append_max.val", "append_avg",
        "concat_len", "concat_max.val", "concat_avg");

    assertThat(
      rows,
      containsInAnyOrder(
        contains("values1-100.csv", "101", "10001", "149", "101", "20000", "248"),
        contains("values101-1000.csv", "901", "10001", "561", "901", "20000", "572"),
        contains("values1001-10000.csv", "9001", "10001", "5501", "9001", "20000", "5502")
      )
    );
  }

  @Test
  public void canSkipInvalidTuples() throws Exception {
    pipeline = ""
        + "input(value: { to_list(bookmark('dodgy')) as list }) "
        + "-> "
        // NB length should only return the valid tuples
        + "select({*, length(list) as len })"
        + "->"
        + "unnest(list)"
        + "->"
        + "group(select: { max(len) as len, count(*), sum(list.numeric_ish) as total })";

    evalCommand.pipelineFile = pipeline;
    assertNull(evalCommand.run());

    // we get invalid tuple warnings this time
    assertThat(this.collectedSinkProblems, not(empty()));

    rows = openCsv("group.csv", "len", "count", "total");

    assertThat(rows, contains(contains("2", "2", "7")));
  }

  @Test
  public void getErrorUnnestingRelationWithSkipInvalidTuplesDisabled() throws Exception {
    pipeline = ""
        + "input(value: { to_list(bookmark('invalid')) as list }) "
        + "-> "
        + "unnest(list)"
        + "->"
        + "group(select: { count(*) })";

    evalCommand.pipelineFile = pipeline;
    ExitException ex = assertThrows(ExitException.class, () -> evalCommand.run());
    assertThat(ex.getProblem(), hasAncestorProblem(equalIgnoringChildren(
        InputDataProblems.get().invalidTupleError()
    )));
  }

  private void evaluate() {
    evalCommand.pipelineFile = pipeline;
    System.err.println(pipeline);
    assertNull(evalCommand.run());

    assertThat(this.collectedSinkProblems, empty());
  }

}
