/*
 * 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.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;

import org.jline.terminal.impl.DumbTerminal;
import org.junit.Before;
import org.junit.Test;

import com.google.common.collect.Lists;

import lombok.Getter;
import nz.org.riskscape.engine.Assert;
import nz.org.riskscape.engine.Engine;
import nz.org.riskscape.engine.OsUtils;
import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.plugin.ExtensionPoint;
import nz.org.riskscape.engine.plugin.ExtensionPoints;
import nz.org.riskscape.engine.plugin.Plugin;
import nz.org.riskscape.engine.plugin.PluginDescriptor;
import nz.org.riskscape.engine.plugin.PluginRepository;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.spi.CliCommand;
import nz.org.riskscape.picocli.CommandLine;
import nz.org.riskscape.picocli.CommandLine.Command;
import nz.org.riskscape.problem.Problems;

public class MainTest extends TerminalTestHelper {

  public static class ExitHookException extends RuntimeException {

    @Getter
    private Integer code;

    public ExitHookException(Integer code, String string) {
      super(string);
      this.code = code;
    }
  }

  private Main main;
  ExtensionPoints extensionPoints;
  PluginRepository mockPluginRepo = mock(PluginRepository.class);
  CliBootstrap cliBootstrap = new CliBootstrap();
  org.jline.terminal.Terminal jlineTerminal;
  Consumer<Integer> exithook = (code) -> {
    throw new ExitHookException(code, errorStream.toString());
  };
  private Plugin plugin;

  @Before
  public void setup() throws Exception {
    jlineTerminal = mock(DumbTerminal.class);
    main = new Main(jlineTerminal, terminal, messages, exithook, mockPluginRepo,
        cliBootstrap);
    cliBootstrap.setPluginRepository(mockPluginRepo);
    System.setProperty("riskscape.app_home", ".");

    extensionPoints = cliBootstrap.getExtensionPoints();
    extensionPoints.add(new ExtensionPoint<>(CliCommand.class));

    plugin = mock(Plugin.class);
    when(mockPluginRepo.getActivated()).thenReturn(Collections.singletonList(plugin));
    PluginDescriptor descriptor = mock(PluginDescriptor.class);
    when(plugin.getDescriptor()).thenReturn(descriptor);
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  CliCommand wrapCommand(Object command) {
    return new CliCommand(command.getClass()) {
      @Override
      public Object newInstance() {
        return command;
      }
    };
  }

  enum Category {
    LEGACY,
    NORMAL;
  }

  @Command(name = "dummy", description = "dummy")
  public static class TestEngineCommand extends EngineOnlyCommand {

    public Engine setEngine;
    public Object toReturn;

    @CommandLine.Option(names={"-c", "--category"}, paramLabel = "<category>")
    List<Category> categories = Lists.newArrayList(Category.NORMAL);

    @Override
    public Object doCommand(Engine useEngine) {
      this.setEngine = useEngine;
      return toReturn;
    }
  }

  @Command(name = "stub", description = "dummy")
  public static class TestStubCommand extends StubCommand {
  }

  @Test
  public void noArgumentsPrintsOutUsage() throws Exception {
    ExitHookException ex = Assert.assertThrows(ExitHookException.class, ()
        -> main.runMain(new String[] {}));
    assertThat(ex.code, equalTo(1));
    assertThat(ex.getMessage(), containsString("Usage: riskscape"));

    //Check that translations are loaded
    //we match on some word characters rather than actual text. We only care that is has content.
    Pattern pluginPattern = Pattern.compile(".*plugins\\s+\\w+.*", Pattern.DOTALL);
    assertThat(ex.getMessage(), matchesPattern(pluginPattern));
  }

  @Test
  public void dashDashHelpPrintsOutUsage() throws Exception {
    ExitHookException ex = Assert.assertThrows(ExitHookException.class, ()
        -> main.runMain(new String[] {"--help"}));
    assertThat(ex.code, equalTo(0));

    //Check that translations are loaded
    //we match on some word characters rather than actual text. We only care that is has content.
    Pattern pluginPattern = Pattern.compile(".*plugins\\s+\\w+.*", Pattern.DOTALL);
    assertThat(ex.getMessage(), matchesPattern(pluginPattern));
  }

  @Test
  public void testThatEngineCommandsAreGivenTheEngine() throws Exception {
    TestEngineCommand command = new TestEngineCommand();

    extensionPoints.addFeature(wrapCommand(command));

    main.runMain(new String[] {"dummy"});

    assertNotNull(command.setEngine);
    assertThat(command.categories, contains(Category.NORMAL));
  }

  @Test
  public void testThatEnumsCanBeSetWithValue() throws Exception {
    TestEngineCommand command = new TestEngineCommand();

    extensionPoints.addFeature(wrapCommand(command));

    main.runMain(new String[] {"dummy", "--category", "LEGACY"});

    assertNotNull(command.setEngine);
    assertThat(command.categories, contains(Category.LEGACY));
  }

  @Test
  public void testThatEnumsCanBeSetWithLowerCaseValue() throws Exception {
    TestEngineCommand command = new TestEngineCommand();

    extensionPoints.addFeature(wrapCommand(command));

    main.runMain(new String[] {"dummy", "--category", "legacy"});

    assertNotNull(command.setEngine);
    assertThat(command.categories, contains(Category.LEGACY));
  }

  @Test
  public void testThatEnumsCanBeSetWithMixedCaseValue() throws Exception {
    TestEngineCommand command = new TestEngineCommand();

    extensionPoints.addFeature(wrapCommand(command));

    main.runMain(new String[] {"dummy", "--category", "LegACY"});

    assertNotNull(command.setEngine);
    assertThat(command.categories, contains(Category.LEGACY));
  }

  @Test
  public void testThatStubCommandsPrintUsageInformation() {
    extensionPoints.addFeature(new CliCommand<>(TestStubCommand.class));

    ExitHookException ex = Assert.assertThrows(ExitHookException.class, ()
        -> main.runMain(new String[] {"stub"}));

    assertThat(ex.code, equalTo(1));
    assertThat(ex.getMessage(), containsString("Error: No subcommand argument given"));
  }

  @Test
  public void testThatAttemptingToRegisterCommandsWithTheSameNameGivesAnError() throws Exception {
    TestEngineCommand stubCommand1 = new TestEngineCommand();
    TestEngineCommand stubCommand2 = new TestEngineCommand();

    extensionPoints.addFeature(wrapCommand(stubCommand1));
    extensionPoints.addFeature(wrapCommand(stubCommand2));

    main.runMain(new String[] {"dummy"});

    assertNotNull(stubCommand1.setEngine);
    assertNull(stubCommand2.setEngine);

    assertThat(errorStream.toString(), containsString(String.format(
        "WARNING: Ignoring command '%s' named '%s' - already added by '%s'",
        stubCommand2, "dummy", stubCommand1)));
  }

  @Test
  public void testThatACommandReturningAProblemPrintsThatProblem() throws Exception {
    TestEngineCommand command = new TestEngineCommand();
    command.toReturn = Problems.foundWith("carrots", GeneralProblems.get().failedObjectLookedUp("foo", Bookmark.class));

    extensionPoints.addFeature(wrapCommand(command));

    ExitHookException ex = Assert.assertThrows(ExitHookException.class, ()
        -> main.runMain(new String[] {"dummy"}));

    // returning a problem should be a fail
    assertThat(ex.code, equalTo(1));

    // make sure we have a trailing newline after this kind of error (we had a regression for this)
    assertTrue(errorStream.toString().endsWith(OsUtils.LINE_SEPARATOR));
    // note that we'll also get warnings here about the app home not having a
    // plugins dir or an api.jar, but we're less interested in these
    List<String> errorLines = Arrays.asList(errorStream.toString().split(OsUtils.LINE_SEPARATOR));
    assertThat(errorLines, hasItems(
        "Problems found with carrots",
        "  - The bookmark 'foo' is not valid due to the following problems"
    ));
  }
}
