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

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

import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.junit.Before;
import org.junit.Test;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;

import com.google.common.collect.ImmutableMap;

import nz.org.riskscape.engine.Assert;
import nz.org.riskscape.engine.Matchers;
import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.cli.tests.TableMatchers;
import nz.org.riskscape.engine.coverage.TypedCoverage;
import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.data.BookmarkProblems;
import nz.org.riskscape.engine.data.BookmarkResolver;
import nz.org.riskscape.engine.data.PickledDataBookmarkResolver;
import nz.org.riskscape.engine.data.ResolvedBookmark;
import nz.org.riskscape.engine.relation.InvalidTupleException;
import nz.org.riskscape.engine.relation.ListRelation;
import nz.org.riskscape.engine.relation.SpatialMetadata;
import nz.org.riskscape.engine.relation.TupleIterator;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.util.Pair;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;

@SuppressWarnings("unchecked")
public class BookmarksCommandTest extends ApplicationCommandTest {

  Bookmark bookmark1 = new Bookmark("foo", "Foo bookmark", null, URI.create("foo.csv"), ImmutableMap.of());
  Bookmark bookmark2 = new Bookmark("bar", "Bar bookmark", null, URI.create("bar.asc"), ImmutableMap.of());
  ListRelation listRelation = ListRelation.ofValues("foo", "bar", "baz");
  CoordinateReferenceSystem testCrs = TypedCoverage.DEFAULT_CRS;

  TypedCoverage coverage = new TypedCoverage.Empty(Types.TEXT) {
    @Override
    public String toString() {
      return "Excellent Coverage";
    }

    @Override
    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
      return testCrs;
    };
  };

  GeometryFactory geometryFactory = project.getSridSet().getGeometryFactory(project.getDefaultCrs());

  Struct spatialType = Types.GEOMETRY.asStruct();

  ListRelation spatialListRelation = ListRelation.ofValues(
      geometryFactory.createPoint(new Coordinate(1, 1)),
      geometryFactory.createPoint(new Coordinate(10, 10))
  ).withSpatialMetadata(new SpatialMetadata(project.getDefaultCrs(),
      spatialType.getEntry("value")));

  BookmarksCommand.BookmarkListCommand listCommand = new BookmarksCommand.BookmarkListCommand();
  BookmarksCommand.Info infoCommand = new BookmarksCommand.Info();

  List<Problem> problems = new ArrayList<>();

  private BookmarkResolver mockResolver = mock(BookmarkResolver.class);

  @Before
  public void setup() throws Exception {

    addCommandDependencies(listCommand);
    addCommandDependencies(infoCommand);
    when(mockResolver.getId()).thenReturn("MockRes");
    project.getBookmarks().add(bookmark1);
    project.getBookmarks().add(bookmark2);
    engine.getBookmarkResolvers().add(mockResolver);

    when(mockResolver.resolve(eq(bookmark1), any()))
        .thenReturn(Optional.of(ResolvedBookmark.stub(bookmark1, listRelation)));
    when(mockResolver.resolve(eq(bookmark2), any()))
        .thenReturn(Optional.of(ResolvedBookmark.stub(bookmark2, coverage)));
  }

  @Test
  public void canListAllBookmarksInATable() throws Exception {
    Table result = (Table) listCommand.run();

    assertThat(
      result,
      allOf(
        TableMatchers.hasRow(Pair.of("id", "bar"), Pair.of("description", equalTo("Bar bookmark"))),
        TableMatchers.hasRow(Pair.of("id", "foo"), Pair.of("location", equalTo("foo.csv")))
      )
    );
  }

  @Test
  public void anUnresolvedBookmarkIsPrintedOut() throws Exception {
    Bookmark bookmark = new Bookmark("broken", "A broken bookmark", "unsupported", URI.create("test.txt"),
        ImmutableMap.of());
    project.getBookmarks().add(bookmark);
    infoCommand.bookmarkId = "broken";

    ExitException ex = Assert.assertThrows(ExitException.class, () -> infoCommand.run());

    assertThat(
      ex.getProblem(),
      Matchers.equalIgnoringChildren(Problems.foundWith(bookmark))
    );
  }

  @Test
  public void canGetInfoOnAParticularBookmarkToStdout() throws Exception {
    listRelation = spatialListRelation;
    problems.add(Problem.warning("This bookmark is a baddun"));
    ResolvedBookmark resolvedBookmark =
        ResolvedBookmark.stub(bookmark1, listRelation, problems, Collections.emptyList());
    when(mockResolver.resolve(eq(bookmark1), any())).thenReturn(Optional.of(resolvedBookmark));

    infoCommand.bookmarkId = "foo";

    assertNull(infoCommand.run());

    // check a few things
    assertThat(outStream.getLines(), hasItems(
        containsString(""),
        containsString("foo"),
        containsString("Description : Foo bookmark"),
        containsString("Location    : foo.csv"),
        containsString("Problems:"),
        containsString("This bookmark is a baddun"),
        containsString(""),
        containsString("value[Geom]"),
        containsString("CRS (full)  : GEOGCS"),
        containsString("Summarizing..."),
        containsString("Row count   : 2"),
        containsString("Bounds      : EPSG:4326 [1.0 : 10.0 East, 1.0 : 10.0 North]")));
  }

  @Test
  public void canGetInfoOnAUriToStdout() throws Exception {
    PickledDataBookmarkResolver pickledDataBookmarkResolver = new PickledDataBookmarkResolver();
    pickledDataBookmarkResolver.add(ResolvedBookmark.withId("test:foo", listRelation));
    engine.add(pickledDataBookmarkResolver);

    infoCommand.bookmarkId = "test:foo";

    assertNull(infoCommand.run());

    // check that it did actually find it (don't worry about the details though)
    assertThat(outStream.getLines(), hasItems(
        // the test:test is a weird artefact from the pickled bookmark resolver
        containsString("Location    : test:test:foo")
    ));
  }

  @Test
  public void aMissingFileGivesAnError() throws Exception {
    infoCommand.bookmarkId = "/dev/null/test.txt";

    RiskscapeException ex = Assert.assertThrows(RiskscapeException.class, () -> infoCommand.run());

    assertThat(ex.getProblems(), is(BookmarkProblems.get().notBookmarkOrFile("/dev/null/test.txt")));
  }

  @Test
  public void anUnknownPrototypeGivesAnError() throws Exception {
    infoCommand.bookmarkId = "test:test.txt";

    RiskscapeException ex = Assert.assertThrows(RiskscapeException.class, () -> infoCommand.run());

    // this time it gets turned in to a bookmark first, which is a little weird as it gives a nested error, whereas the
    // file path case doesn't
    assertThat(
        ex.getProblem(),
        is(Matchers.hasAncestorProblem(is(BookmarkProblems.get().notBookmarkOrFile("test:test.txt"))))
    );
  }

  @Test
  public void invalidTupleErrorsDuringInfoGiveANiceErrorMessage() throws Exception {
    infoCommand.bookmarkId = "foo";

    listRelation = spy(spatialListRelation);

    when(mockResolver.resolve(eq(bookmark1), any()))
      .thenAnswer(inv -> Optional.of(ResolvedBookmark.withId("foo", listRelation)));

    TupleIterator mockIterator = mock(TupleIterator.class);
    when(mockIterator.next()).thenThrow(new InvalidTupleException(new Tuple(spatialType),
        "Reticulated Splines reticent re-evaluation"));

    when(mockIterator.hasNext()).thenReturn(true);
    when(listRelation.iterator()).thenReturn(mockIterator);

    ExitException thrown = Assert.assertThrows(ExitException.class,
        () -> assertNull(infoCommand.run()));
    assertThat(thrown.getCause(), instanceOf(InvalidTupleException.class));
    assertThat(render(Problems.caught(thrown)),
        containsString("Measuring failed because of invalid data in the bookmark"));
    assertThat(render(Problems.caught(thrown)),
        containsString("Reticulated Splines"));
  }

  @Test
  public void briefInfoOptionAvoidsInvalidTupleErrors() throws Exception {
    infoCommand.bookmarkId = "foo";
    infoCommand.brief = true;

    listRelation = spy(spatialListRelation);
    ResolvedBookmark resolvedBookmark = ResolvedBookmark.stub(bookmark1, listRelation, problems,
        Collections.emptyList());
    when(mockResolver.resolve(eq(bookmark1), any())).thenReturn(Optional.of(resolvedBookmark));

    // throw an exception if we try to iterate the bookmark's data (we shouldn't)
    TupleIterator mockIterator = mock(TupleIterator.class);
    when(mockIterator.next())
        .thenThrow(new InvalidTupleException(new Tuple(spatialType), "Reticulated Splines reticent re-evaluation"));
    when(mockIterator.hasNext()).thenReturn(true);
    when(listRelation.iterator()).thenReturn(mockIterator);

    // should be able to display the bookmark info without triggering exceptions
    assertNull(infoCommand.run());

    // check we display the basic info about the bookmark
    assertThat(outStream.toString(), allOf(
      containsString("foo"),
      containsString("Description : Foo bookmark"),
      containsString("Location    : foo.csv"),
      containsString("value[Geom]"),
      containsString("CRS (full)  : GEOGCS"),
      // but that we don't examine the bookmark data
      not(containsString("Summarizing...")),
      not(containsString("Row count   : 2"))
    ));
  }

  @Test
  public void runtimeErrorsDuringInfoGiveANiceErrorMessage() throws Exception {
    infoCommand.bookmarkId = "foo";

    listRelation = spy(spatialListRelation);
    when(mockResolver.resolve(eq(bookmark1), any()))
      .thenAnswer(inv -> Optional.of(ResolvedBookmark.withId("foo", listRelation)));


    TupleIterator mockIterator = mock(TupleIterator.class);
    when(mockIterator.next()).thenThrow(new RuntimeException("I didn't expect this to happen"));

    when(mockIterator.hasNext()).thenReturn(true);
    when(listRelation.iterator()).thenReturn(mockIterator);

    ExitException thrown = Assert.assertThrows(ExitException.class,
        () -> assertNull(infoCommand.run()));
    assertThat(thrown.getCause(), instanceOf(RuntimeException.class));
    assertThat(thrown.getMessage(), containsString("Inspecting the bookmark failed because of an unexpected error"));
    assertThat(thrown.getMessage(), containsString("I didn't expect this to happen"));
  }

  @Test
  public void translatesBookmarkIdToUseableOutputName() {
    BookmarksCommand.Eval evalCommand = new BookmarksCommand.Eval();

    assertThat(evalCommand.bookmarkIdToOutputName("foo"), is("foo"));
    assertThat(evalCommand.bookmarkIdToOutputName("foo.gpkg"), is("foo"));
    assertThat(evalCommand.bookmarkIdToOutputName("path/to/some/foo.gpkg"), is("foo"));

    assertThat(evalCommand.bookmarkIdToOutputName("https://foo.com/"), is("foo"));
    assertThat(evalCommand.bookmarkIdToOutputName("https://foo.com/bar/"), is("bar"));
    assertThat(evalCommand.bookmarkIdToOutputName("https://foo.bar.com/"), is("foo.bar"));
  }




}
