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

import static nz.org.riskscape.engine.problem.ProblemMatchers.*;

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

import java.net.URI;
import java.util.Arrays;
import java.util.List;

import org.junit.Before;
import org.junit.Test;

import com.google.common.collect.ImmutableMap;

import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.ResultOrProblems;

@SuppressWarnings("unchecked")
public class BookmarkFactoryTest extends ProjectTest {

  BookmarkFactory undertest;
  Bookmark bookmark;

  @Before
  public void setup() throws Exception {
    undertest = new BookmarkFactory();
  }

  @Test
  public void canCreateRelationBookmark() throws Exception {
    populate(
        "# just a comment line",
        "[bookmark building]",
        "    description = test description \\",
        "                  spanning two lines",
        "location=    file.shp");


    ResultOrProblems<Bookmark> boe = getBookmarkOr("building");
    assertTrue(boe.getProblems().isEmpty());
    bookmark = boe.get();

    assertNotNull(bookmark);
    assertEquals("building", bookmark.getId());
    assertEquals("test description spanning two lines", bookmark.getDescription());
    // this also asserts that the location is relative to the bookmark.ini file
    assertEquals(
      config.getSection("bookmark building").get().getLocation().resolve("file.shp"),
      bookmark.getLocation()
    );
    assertTrue(bookmark.getLocation().toString().endsWith("file.shp"));
    assertEquals(
        ImmutableMap.of("description", Arrays.asList(bookmark.getDescription()), "location", Arrays.asList("file.shp")),
        bookmark.getUnparsed());
    assertFalse(bookmark.isFromURI());
  }

  @Test
  public void canCreateMultipleBookmarksFromSameFileEvenIfSomeAreBad() throws Exception {
    populate(
        "# just a comment line",
        "[bookmark bookmark0]",
        "description = test description0      ",
        "location= bookmark0.shp",
        "[bookmark bookmark1]",
        "description = test description1      ",
        "location= bookmark1.shp",
        "[bookmark bookmark2]",
        "description = test description2      ",
        "location= bookmark2.shp",
        "[bookmark bookmark-broken]",
        "description = has no location or format");

    for (int i = 0; i < 3; i++) {
      ResultOrProblems<Bookmark> boe = getBookmarkOr("bookmark" + i);
      assertTrue(boe.getProblems().isEmpty());
      bookmark = boe.get();

      assertNotNull(bookmark);
      assertEquals("bookmark" + i, bookmark.getId());
      assertEquals("test description" + i, bookmark.getDescription());
      assertTrue(bookmark.getLocation().toString().endsWith(String.format("bookmark%d.shp", i)));
    }
    ResultOrProblems<Bookmark> boe = getBookmarkOr("bookmark-broken");

    assertTrue(boe.hasProblems(Problem.Severity.ERROR));
    assertParamRequired("location", boe);
    assertNoError("format", boe);
  }

  @Test
  public void canCreateBookmarkWhenExtensionAndFormatDiffer() throws Exception {
    //The format takes presidence.
    populate("# just a comment line",
        "[bookmark hazard]",
        "    description = test description      ",
        "format=shapefile",
        "location=    file.tiff");

    bookmark = getBookmark("hazard");
    assertNotNull(bookmark);
    assertEquals("hazard", bookmark.getId());
    assertEquals("shapefile", bookmark.getFormat());
    assertEquals("test description", bookmark.getDescription());
    assertTrue(bookmark.getLocation().toString().endsWith("file.tiff"));
  }

  @Test
  public void nonFileLocationsHaveNoFile() throws Exception {
    populate(
        "# just a comment line",
        "[bookmark hazard]",
        "    description = test description      ",
        "format=shapefile",
        "location=http://localhost:8080/my-files/file.tiff");

    bookmark = getBookmark("hazard");
    assertNotNull(bookmark);
    assertThat(bookmark.getLocation().getScheme(), is("http")); // not file:// scheme
  }

  @Test
  public void urisAreEscapedWhenRequired() throws Exception {
    populate(
        "# just a comment line",
        "[bookmark building]",
        "    description = test description \\",
        "                  spanning two lines",
        "location=    file:///home/riskscape/my file.shp");


    ResultOrProblems<Bookmark> boe = getBookmarkOr("building");
    assertTrue(boe.getProblems().isEmpty());
    bookmark = boe.get();

    assertNotNull(bookmark);
    assertEquals("building", bookmark.getId());
    assertEquals(
      "file:///home/riskscape/my%20file.shp",
      bookmark.getLocation().toString()
    );
  }

  @Test
  public void exceptionMessageContainsAllReasons() throws Exception {
    populate(
        "# just a comment line",
        "[bookmark bookmark]");

    ResultOrProblems<Bookmark> result = getBookmarkOr("bookmark");

    assertParamRequired("location", result);
    assertNoError("format", result); // Don't complain about format if location is missing
  }

  @Test
  public void canCreateBookMarkWithMissingDescription() throws Exception {
    populate(
        "# just a comment line",
        "[bookmark building]",
        "location=    file.random");

    assertThat(getBookmarkOr("building").getProblems(), empty());
  }

  @Test
  public void canCreateBookMarkWithEmptyDescription() throws Exception {
    populate(
        "# just a comment line",
        "[bookmark building]",
        "    description =       ",
        "location=    file.random");


    assertThat(getBookmarkOr("building").getProblems(), empty());
  }

  @Test
  public void cannotCreateBookMarkWithMissingLocation() throws Exception {
    populate(
        "# just a comment line",
        "[bookmark building]",
        "description= blah blah");

    assertParamRequired("location", getBookmarkOr("building"));
  }

  @Test
  public void cannotCreateBookMarkWithEmptyLocation() throws Exception {
    populate(
        "# just a comment line",
        "[bookmark building]",
        "    description =    blah blah   ",
        "location=");

    assertParamRequired("location", getBookmarkOr("building"));
  }

  @Test
  public void urlParametersFromLocationComeThroughInUnparsedParams() throws Exception {
    URI uri = BookmarkFactory.uriFromLocation("bar.csv?type=foo-type", URI.create("file:///foo")).get();
    bookmark = Bookmark.fromURI(uri);
    assertEquals(ImmutableMap.of("type", Arrays.asList("foo-type")), bookmark.getUnparsed());
    assertTrue(bookmark.isFromURI());
  }

  @Test
  public void nonUrlLocationsCanIncludeUrlUnsafeCharacters() throws Exception {
    URI uri = BookmarkFactory.uriFromLocation("/blep/this is bar.csv?type=foo-type",
        URI.create("file:///foo/bar/baz/bookmarks.ini")).get();

    bookmark = Bookmark.fromURI(uri);

    // on windows it's /C:/blep ...
    assertTrue(bookmark.getLocation().getPath().contains("/blep/this is bar.csv"));
    assertEquals(ImmutableMap.of("type", Arrays.asList("foo-type")), bookmark.getUnparsed());
  }

  @Test
  public void uriFromLocationIsCorrectRelativeToNonFileSchemes() throws Exception {
    assertThat(BookmarkFactory.uriFromLocation("directory/file.txt", URI.create("custom://project/")).get(),
        is(URI.create("custom://project/directory/file.txt")));
    assertThat(BookmarkFactory.uriFromLocation("directory/file.txt", URI.create("custom://project/somefile.txt")).get(),
        is(URI.create("custom://project/directory/file.txt")));
  }

  @Test
  public void duplicatedUrlParametersAreConcatenated() throws Exception {
    URI uri = BookmarkFactory.uriFromLocation("bar.csv?type=foo-type&type=bar-type", URI.create("file:///foo")).get();
    bookmark = Bookmark.fromURI(uri);
    assertEquals(ImmutableMap.of("type", Arrays.asList("foo-type", "bar-type")), bookmark.getUnparsed());
  }

  @Test
  public void invalidRelativeURIsAreHandled() throws Exception {
    // important that this is not file, as files are treated differently
    URI relative = URI.create("http://foo/bar/");

    assertThat(
        BookmarkFactory.uriFromLocation("####???###?#?#?#", relative).get(),
        // the # characters get encoded
        is(URI.create("http://foo/bar/%23%23%23%23???%23%23%23?%23?%23?%23"))
    );

  }

  @Test
  public void emptyParameterValuesInQueryIsOK() throws Exception {
    bookmark = Bookmark.fromURI(URI.create("some-data?foo=bar&baz=&foobar"));
  }

  @Test
  public void canHandleProblematicBookmarkIds() throws Exception {
    // IdentifiedObjectBuilder replaces `getKeyword() + " "` with "".
    // We want to check that we don't end up with an empty ID in these cases.
    // (we just end up with id="bookmark" because the extra whitespace gets trimmed)
    populate(
        "[bookmark    bookmark ]",
        "location= file.shp");

    ResultOrProblems<Bookmark> boe = getBookmarkOr("bookmark");
    assertTrue(boe.getProblems().isEmpty());
    bookmark = boe.get();

    assertNotNull(bookmark);
    assertEquals("bookmark", bookmark.getId());
    assertEquals(URI.create("test:/file.shp"), bookmark.getLocation());
  }

  private void assertNoError(String partialMessage, ResultOrProblems<Bookmark> resultOr) {
    for (Problem problem : resultOr.getProblems()) {
      if (problem.getMessage().contains(partialMessage)) {
        fail(String.format("Expected to not find '%s' but found it in: %s", partialMessage, problem));
      }
    }

    return;
  }

  private void assertParamRequired(String paramName, ResultOrProblems<Bookmark> resultOr) {
    List<Problem> problems = resultOr.filterProblems(Parameter.class);

    assertThat(problems, contains(
      isProblem(GeneralProblems.class, (r, f) -> f.required(r.match(namedArg(Parameter.class, paramName))))
    ));
  }

  private ResultOrProblems<Bookmark> getBookmarkOr(String id) {
    return project.getBookmarks().getOr(id);
  }

  private Bookmark getBookmark(String id) {
    return getBookmarkOr(id).get();
  }
}
