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

import java.io.ByteArrayInputStream;
import java.util.Arrays;

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

import com.google.common.collect.Lists;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

import nz.org.riskscape.cli.AnsiHelper;
import nz.org.riskscape.cli.Terminal;
import nz.org.riskscape.engine.CapturingStream;
import nz.org.riskscape.engine.OsUtils;
import nz.org.riskscape.engine.cli.Table.Property;
import nz.org.riskscape.engine.i18n.MessageKey;
import nz.org.riskscape.engine.i18n.MessageSource;
import nz.org.riskscape.engine.i18n.Messages;
import nz.org.riskscape.engine.i18n.MutableMessageSource;
import nz.org.riskscape.engine.i18n.RiskscapeMessage;
import nz.org.riskscape.picocli.CommandLine.Help.Ansi;

public class TableTest implements AnsiHelper {

  @BeforeClass
  public static void disableAnsi() {
    Table.defaultAnsi = Ansi.OFF;
  }

  @RequiredArgsConstructor
  public static class Data {
    @Getter
    final String name;
    @Getter
    final String description;
    @Getter
    final MessageKey i18nMessage;

    public Data(String name, String description) {
      this.name = name;
      this.description = description;
      this.i18nMessage = RiskscapeMessage.of(name);
    }
  }

  CapturingStream outStream = new CapturingStream();
  TestTerminal terminal = new TestTerminal(System.err, outStream.getPrintStream(),
      // NB without an empty string here, the paging test blocks forever waiting for input
      new ByteArrayInputStream("".getBytes()),
      (Messages) null);

  @Override
  public Ansi getAnsi() {
    return Ansi.OFF;
  }

  @Before
  public void setupDefaultTtyWidth() {
    // most tests assume output is 80-chars wide
    terminal.setDisplayWidth(80);
  }

  @Test
  public void willPrintASimpleTable() {
    Table table = Table.fromStrings(Arrays.asList(
        Arrays.asList("these", "are", "headings"),
        Arrays.asList("foo", "bar", "baz"),
        Arrays.asList("cat", "dog", "ewe")
    ));

    table.print(terminal);

    String nl = System.getProperty("line.separator");

    String expected =
        "+-----+---+--------+" + nl +
        "|these|are|headings|" + nl +
        "+-----+---+--------+" + nl +
        "|cat  |dog|ewe     |" + nl +
        "|     |   |        |" + nl +
        "|foo  |bar|baz     |" + nl +
        "+-----+---+--------+" + nl +
        "";

    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willPrintSimpleTableWithUnicode() {
    terminal.setDisplayWidth(25);
    Table table = Table.fromStrings(Arrays.asList(
        Arrays.asList("𝑓()"),
        Arrays.asList("foobar"),
        Arrays.asList("baz")
    ));
    table.print(terminal);

    String nl = System.getProperty("line.separator");

    String expected =
        "+------+" + nl +
        "|𝑓()   |" + nl +
        "+------+" + nl +
        "|baz   |" + nl +
        "|      |" + nl +
        "|foobar|" + nl +
        "+------+" + nl +
        "";
    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willPrintTableAndPadWideUnicodeCharacters() {
    // in this example the Hiragana characters need some padding added
    Table table = Table.fromStrings(Arrays.asList(
        Arrays.asList("平"),
        Arrays.asList("foo"),
        Arrays.asList("baz")
    ));
    table.print(terminal);

    String nl = System.getProperty("line.separator");

    String expected =
        "+---+" + nl +
        "|平 |" + nl +
        "+---+" + nl +
        "|baz|" + nl +
        "|   |" + nl +
        "|foo|" + nl +
        "+---+" + nl +
        "";

    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willPrintTableWithWideUnicode() {

    // in this example the Hiragana characters are the widest on screen
    Table table = Table.fromStrings(Arrays.asList(
        Arrays.asList("平仮名"),
        Arrays.asList("foo"),
        Arrays.asList("baz")
    ));
    table.print(terminal);

    String nl = System.getProperty("line.separator");

    String expected =
        "+------+" + nl +
        "|平仮名|" + nl +
        "+------+" + nl +
        "|baz   |" + nl +
        "|      |" + nl +
        "|foo   |" + nl +
        "+------+" + nl +
        "";

    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willPrintTableFromCollection() {
    Table table = Table.fromList(
      Arrays.asList(
        new Data("Ronny", "a risk analyst"),
        new Data("Janice", "admin type")
      ),
      Data.class,
      new MutableMessageSource(),
      Arrays.asList(
          Property.of("name"),
          Property.of("description")
      )
    );

    table.print(terminal);

    String nl = System.getProperty("line.separator");

    String expected =
        "+------+--------------+" + nl +
        "|name  |description   |" + nl +
        "+------+--------------+" + nl +
        "|Janice|admin type    |" + nl +
        "|      |              |" + nl +
        "|Ronny |a risk analyst|" + nl +
        "+------+--------------+" + nl +
        "";

    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willPrintTableWithNullValue() {
    Table table = Table.fromList(
      Arrays.asList(
        new Data("Ronny", "a risk analyst"),
        new Data("Janice", "admin type")
      ),
      Data.class,
      new MutableMessageSource(),
        // simulate a bad i18n key lookup that results in a null value
      Arrays.asList(
          Property.of("name", Data::getName),
          Property.of("oopsie", t -> null)
      )
    );
    table.print(terminal);
    String nl = System.getProperty("line.separator");

    // a developer's mistake shouldn't crash the system
    String expected =
        "+------+------+" + nl +
        "|name  |oopsie|" + nl +
        "+------+------+" + nl +
        "|Janice|      |" + nl +
        "|      |      |" + nl +
        "|Ronny |      |" + nl +
        "+------+------+" + nl +
        "";

    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willTranslateHeaderNames() {
    MessageSource messageSource = mock(MessageSource.class);
    when(messageSource.getMessage(
        RiskscapeMessage.withDefault("nz.org.riskscape.engine.cli.TableTest.Data.name.label", "name")))
        .thenReturn("NAME");
    when(messageSource.getMessage(
        RiskscapeMessage.withDefault("nz.org.riskscape.engine.cli.TableTest.Data.description.label", "description")))
        .thenReturn("DESCRIPTION");
    Table table = Table.fromList(
        Lists.newArrayList(
            new Data("Ronny", "a risk analyst"),
            new Data("Janice", "admin type")
        ),
        Data.class,
        messageSource,
        Arrays.asList(
            Property.of("name", Data::getName),
            Property.of("description", Data::getDescription)
        )
    );

    table.print(terminal);

    String nl = System.getProperty("line.separator");

    String expected =
        "+------+--------------+" + nl +
        "|NAME  |DESCRIPTION   |" + nl +
        "+------+--------------+" + nl +
        "|Janice|admin type    |" + nl +
        "|      |              |" + nl +
        "|Ronny |a risk analyst|" + nl +
        "+------+--------------+" + nl +
        "";

    assertEquals(expected, outStream.toString());
  }


  @Test
  public void willPrintTableFromCollectionWithLineFeeds() {
    Table table = Table.fromList(Lists.newArrayList(
        new Data("Ronny", String.format("a%nrisk%nanalyst")),
        new Data("Janice", "admin type")
    ), "name", "description");

    table.print(terminal);

    String nl = System.getProperty("line.separator");

    String expected =
        "+------+-----------+" + nl +
        "|name  |description|" + nl +
        "+------+-----------+" + nl +
        "|Janice|admin type |" + nl +
        "|      |           |" + nl +
        "|Ronny |a          |" + nl +
        "|      |risk       |" + nl +
        "|      |analyst    |" + nl +
        "+------+-----------+" + nl +
        "";

    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willPrintTableFromCollectionWithLineFeeds2() {
    String numbers = String.format(
        "one%n"
        + "two%n"
        + "three%n"
        + "four%n"
        + "five%n"
    );

    String blurb = String.format(
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit.%n"
        + "Etiam consequat purus eu odio posuere, a  magna et ligula tristique feugiat lacinia sapien feugiat.%n"
        + "Maecenas suscipit libero magna, nec vulputate odio suscipit non.%n"
    );

    Table table = Table.fromList(Lists.newArrayList(
        new Data(numbers, blurb)
    ), "name", "description");

    table.print(terminal);

    String nl = System.getProperty("line.separator");

    String expected =
        "+------+-----------------------------------------------------------------------+" + nl +
        "|name  |description                                                            |" + nl +
        "+------+-----------------------------------------------------------------------+" + nl +
        "|one   |Lorem ipsum dolor sit amet, consectetur adipiscing elit.               |" + nl +
        "|two   |Etiam consequat purus eu odio posuere, a  magna et ligula tristique    |" + nl +
        "|three |feugiat lacinia sapien feugiat.                                        |" + nl +
        "|four  |Maecenas suscipit libero magna, nec vulputate odio suscipit non.       |" + nl +
        "|five  |                                                                       |" + nl +
        "+------+-----------------------------------------------------------------------+" + nl +
        "";

    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willWrapColumnsThatAreTooLarge_AndKeepPuncuation() {

    String blurb = String.format(
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit.%n"
    );

    Table table = Table.fromList(Lists.newArrayList(
        new Data("test", blurb)
    ), "description");

    terminal.setDisplayWidth(28);
    table.print(terminal);

    String nl = OsUtils.LINE_SEPARATOR;

    String expected =
        "+--------------------------+" + nl +
        "|description               |" + nl +
        "+--------------------------+" + nl +
        "|Lorem ipsum dolor sit     |" + nl +   //The white space at the end of this line is enough for " amet"
        "|amet, consectetur         |" + nl +
        "|adipiscing elit.          |" + nl +
        "+--------------------------+" + nl +
        "";

    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willWrapColumnsThatAreTooLarge() {
    Table table = Table.fromStrings(Arrays.asList(
        Arrays.asList("foo", "bar", "baz"),
        Arrays.asList("foo can bar", "bar will foo", "baz should not really foo unless bar is busy"),
        Arrays.asList("cat", "dog must chase cat ewe know?", "ewe")
    ));

    terminal.setDisplayWidth(80);
    table.print(terminal);


    String expected =
        "+------------+--------------------------------+--------------------------------+\n" +
        "|foo         |bar                             |baz                             |\n" +
        "+------------+--------------------------------+--------------------------------+\n" +
        "|cat         |dog must chase cat ewe know?    |ewe                             |\n" +
        "|            |                                |                                |\n" +
        "|foo can bar |bar will foo                    |baz should not really foo unless|\n" +
        "|            |                                |bar is busy                     |\n" +
        "+------------+--------------------------------+--------------------------------+\n";

    assertEquals(expected, outStream.toString());

    outStream.reset();

    expected =
        "+-----------+------------+-------------+\n" +
        "|foo        |bar         |baz          |\n" +
        "+-----------+------------+-------------+\n" +
        "|cat        |dog must    |ewe          |\n" +
        "|           |chase cat   |             |\n" +
        "|           |ewe know?   |             |\n" +
        "|           |            |             |\n" +
        "|foo can bar|bar will foo|baz should   |\n" +
        "|           |            |not really   |\n" +
        "|           |            |foo unless   |\n" +
        "|           |            |bar is busy  |\n" +
        "+-----------+------------+-------------+\n";

    terminal.setDisplayWidth(40);
    table.print(terminal);
    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willWrapParagraphsInACell() {
    assertEquals(
      Table.rowToText(Arrays.asList("foo", "bar")),
      Table.splitText(5, applyStyles("foo\nbar"))
    );

    assertEquals(
      Table.rowToText(Arrays.asList("foo ", "bar", "baz")),
      Table.splitText(5, applyStyles("foo bar\nbaz"))
    );
  }

  @Test
  public void willWrapWordsInACell() {
    assertEquals(
      Table.rowToText(Arrays.asList("foo bar,", "to the ", "baz!")),
      Table.splitText(8, applyStyles("foo bar, to the baz!"))
    );
  }

  @Test
  public void willWrapWordsThatAreTooLargeInACell_RM120() {
    assertEquals(
      Table.rowToText(Arrays.asList("foob" + Terminal.ELLIPSIS)),
      Table.splitText(5, applyStyles("foobar"))
    );

    assertEquals(
      Table.rowToText(Arrays.asList("f ", "bars")),
      Table.splitText(5, applyStyles("f bars"))
    );
  }

  @Test
  public void willPaginateToDisplaySize() {
    String nl = OsUtils.LINE_SEPARATOR;
    Table table = Table.fromStrings(Arrays.asList(
            Arrays.asList("foo can bar", "bar will foo", "baz should not really foo unless bar is busy"),
            Arrays.asList("cat", "dog must chase cat ewe know?", "ewe"),
            Arrays.asList("cat", "calculates minimum input for maximum output", "ewe"),
            Arrays.asList("dog", "computes run times", "foo"),
            Arrays.asList("dog", "sits", "cat"),
            Arrays.asList("ewe", "sees horse wants hay too", "horse")
    ));
    terminal.setTTY(true);
    terminal.setDisplayWidth(55);
    terminal.setDisplayHeight(14);
    table.print(terminal);

    String expected =
        "+-----------+--------------------+--------------------+" + nl +
        "|foo can bar|bar will foo        |baz should not      |" + nl +
        "|           |                    |really foo unless   |" + nl +
        "|           |                    |bar is busy         |" + nl +
        "+-----------+--------------------+--------------------+" + nl +
        "|cat        |dog must chase cat  |ewe                 |" + nl +
        "|           |ewe know?           |                    |" + nl +
        "|           |                    |                    |" + nl +
        "|cat        |calculates minimum  |ewe                 |" + nl +
        "|           |input for maximum   |                    |" + nl +
        "|           |output              |                    |" + nl +
        "|           |                    |                    |" + nl +
        "|dog        |computes run times  |foo                 |" + nl +
        "Press enter to continue..." +
        "|           |                    |                    |" + nl +
        "|dog        |sits                |cat                 |" + nl +
        "|           |                    |                    |" + nl +
        "|ewe        |sees horse wants hay|horse               |" + nl +
        "|           |too                 |                    |" + nl +
        "+-----------+--------------------+--------------------+" + nl;

    assertEquals(expected, outStream.toString());

    outStream.reset();

    expected =
        "+-----------+--------------------+--------------------+" + nl +
        "|foo can bar|bar will foo        |baz should not      |" + nl +
        "|           |                    |really foo unless   |" + nl +
        "|           |                    |bar is busy         |" + nl +
        "+-----------+--------------------+--------------------+" + nl +
        "|cat        |dog must chase cat  |ewe                 |" + nl +
        "Press enter to continue..." +
        "|           |ewe know?           |                    |" + nl +
        "|           |                    |                    |" + nl +
        "|cat        |calculates minimum  |ewe                 |" + nl +
        "|           |input for maximum   |                    |" + nl +
        "|           |output              |                    |" + nl +
        "Press enter to continue..." +
        "|           |                    |                    |" + nl +
        "|dog        |computes run times  |foo                 |" + nl +
        "|           |                    |                    |" + nl +
        "|dog        |sits                |cat                 |" + nl +
        "|           |                    |                    |" + nl +
        "|ewe        |sees horse wants hay|horse               |" + nl +
        "Press enter to continue..." +
        "|           |too                 |                    |" + nl +
        "+-----------+--------------------+--------------------+" + nl;

    terminal.setTTY(true);
    terminal.setDisplayWidth(55);
    terminal.setDisplayHeight(6);

    table.print(terminal);
    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willNotPaginateWhenNotTTY() {
    String nl = OsUtils.LINE_SEPARATOR;
    Table table = Table.fromStrings(Arrays.asList(
            Arrays.asList("foo can bar", "bar will foo", "baz should not really foo unless bar is busy"),
            Arrays.asList("cat", "dog must chase cat ewe know?", "ewe"),
            Arrays.asList("cat", "calculates minimum input for maximum output", "ewe"),
            Arrays.asList("dog", "computes run times", "foo"),
            Arrays.asList("dog", "sits", "cat"),
            Arrays.asList("ewe", "sees horse wants hay too", "horse")
    ));

    String expected =
        "+-----------+--------------------+--------------------+" + nl +
        "|foo can bar|bar will foo        |baz should not      |" + nl +
        "|           |                    |really foo unless   |" + nl +
        "|           |                    |bar is busy         |" + nl +
        "+-----------+--------------------+--------------------+" + nl +
        "|cat        |dog must chase cat  |ewe                 |" + nl +
        "|           |ewe know?           |                    |" + nl +
        "|           |                    |                    |" + nl +
        "|cat        |calculates minimum  |ewe                 |" + nl +
        "|           |input for maximum   |                    |" + nl +
        "|           |output              |                    |" + nl +
        "|           |                    |                    |" + nl +
        "|dog        |computes run times  |foo                 |" + nl +
        "|           |                    |                    |" + nl +
        "|dog        |sits                |cat                 |" + nl +
        "|           |                    |                    |" + nl +
        "|ewe        |sees horse wants hay|horse               |" + nl +
        "|           |too                 |                    |" + nl +
        "+-----------+--------------------+--------------------+" + nl;
    terminal.setTTY(false);
    terminal.setDisplayWidth(55);
    terminal.setDisplayHeight(5);
    table.print(terminal);

    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willNotPromptIfWindowLargerThanData() {
    String nl = OsUtils.LINE_SEPARATOR;
    Table table = Table.fromStrings(Arrays.asList(
            Arrays.asList("foo can bar", "bar will foo", "baz should not really foo unless bar is busy"),
            Arrays.asList("ewe", "sees horse wants hay too", "horse")
    ));
    terminal.setTTY(true);
    terminal.setDisplayWidth(80);
    terminal.setDisplayHeight(14);
    table.print(terminal);

    String expected = "+------------+------------------------+----------------------------------------+" + nl +
            "|foo can bar |bar will foo            |baz should not really foo unless bar is |" + nl +
            "|            |                        |busy                                    |" + nl +
            "+------------+------------------------+----------------------------------------+" + nl +
            "|ewe         |sees horse wants hay too|horse                                   |" + nl +
            "+------------+------------------------+----------------------------------------+"+ nl;

    assertEquals(expected, outStream.toString());
  }

  @Test
  public void willKeepAnyLineFeedsInCellContent() {
    String nl = OsUtils.LINE_SEPARATOR;
    Table table = Table.fromStrings(Arrays.asList(
        Arrays.asList("foo", "bar", "baz"),
        Arrays.asList(
            "foo\nuses\nunix\nline feeds",  // *nix
            "bar\ris old\rschool mac",      // Mac
            "baz\r\nis windows")            // Windows
    ));

    terminal.setTTY(true);
    terminal.setDisplayWidth(80);
    terminal.setDisplayHeight(14);
    table.print(terminal);

    String expected =
            "+----------+----------+----------+" + nl +
            "|foo       |bar       |baz       |" + nl +
            "+----------+----------+----------+" + nl +
            "|foo       |bar       |baz       |" + nl +
            "|uses      |is old    |is windows|" + nl +
            "|unix      |school mac|          |" + nl +
            "|line feeds|          |          |" + nl +
            "+----------+----------+----------+"+ nl;

    assertEquals(expected, outStream.toString());
  }
}
