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

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

import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import lombok.RequiredArgsConstructor;
import nz.org.riskscape.engine.Identified;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import nz.org.riskscape.engine.function.JavaFunction;
import nz.org.riskscape.engine.plugin.ExtensionPoints;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.ProblemCode;

public class MessagesTest {

  static class Bundle extends ResourceBundle {
    public Map<String, String> bundle = new HashMap<>();

    @Override
    protected Object handleGetObject(String key) {
      return bundle.get(key);
    }

    @Override
    public Enumeration<String> getKeys() {
      return Collections.enumeration(bundle.keySet());
    }
  }

  Messages messages;
  JavaFunction function = JavaFunction.withId("foo");

  Bundle help;
  Bundle labels;
  Bundle problems;
  DefaultObjectRenderer mockDefaultRenderer = Mockito.mock(DefaultObjectRenderer.class);
  ExtensionPoints extensionPoints = new ExtensionPoints();
  Locale locale = Locale.getDefault();

  @Before
  public void setup() {
    Map<String, ResourceBundle> bundles = new HashMap<>();
    messages = new DefaultMessages(bundles, mockDefaultRenderer, extensionPoints);
    help = new Bundle();
    labels = new Bundle();
    problems = new Bundle();

    bundles.put("help", help);
    bundles.put("labels", labels);
    bundles.put("problems", problems);
  }

  static class Animal {}
  static class Dog extends Animal {}

  @Test
  public void getMessagesCodesAreLookedUpByIdThenThroughClassHierarcy() throws Exception {

    // never looks on object - that's ridiculous (I think?)
    help.bundle.put("java.lang.Object.object", "fail");

    /*
     * Class lookups
     */
    help.bundle.put(IdentifiedFunction.class.getCanonicalName() + ".by-id.foo.by-id-1", "pass1");

    /*
     * Identified lookups
     */
    // by-id should always find the top-level class/interface that implements
    // Identified, this takes precedence over class look up
    help.bundle.put(IdentifiedFunction.class.getCanonicalName() + ".by-id.foo.by-id-1", "pass-cn-1");
    // simple name allowed for by-id lookups
    help.bundle.put(IdentifiedFunction.class.getSimpleName() + ".by-id.foo.by-id-1", "fail");
    // never use the Identified interface - we always need the extended interface to discriminate
    help.bundle.put(Identified.class.getCanonicalName() + ".by-id.foo.by-id-1", "fail");
    // never use the actual implementation with the by-id thing either
    help.bundle.put(JavaFunction.class.getCanonicalName() + ".by-id-1", "fail");
    // check that simple name works too
    help.bundle.put(IdentifiedFunction.class.getSimpleName() + ".by-id.foo.by-id-2", "pass-sn-2");

    /*
     * Vanilla object lookups and class lookups
     */

    // non-identified things search through class hierarchy, too
    help.bundle.put(Animal.class.getCanonicalName() + ".hierarchy-1", "pass-animal");
    // $ variant not OK
    help.bundle.put(Animal.class.getName() + ".hierarchy-1", "fail");
    // super class takes precendence
    help.bundle.put(Dog.class.getCanonicalName() + ".hierarchy-2", "pass-dog");
    help.bundle.put(Animal.class.getCanonicalName() + ".hierarchy-2", "fail");
    // simple names are never used for class based lookups - too much ambiguity possible
    help.bundle.put(Dog.class.getSimpleName() + ".hierarchy-3", "fail");

    // all the assertions need to happen together 'cos of caching in resource bundle
    assertThat(messages.getMessage(messages.getHelp(), function, "object"), equalTo(Optional.empty()));
    assertThat(messages.getMessage(messages.getHelp(), function, "by-id-1"), equalTo(Optional.of("pass-cn-1")));
    assertThat(messages.getMessage(messages.getHelp(), function, "by-id-2"), equalTo(Optional.of("pass-sn-2")));
    assertThat(messages.getMessage(messages.getHelp(), new Dog(), "hierarchy-1"), equalTo(Optional.of("pass-animal")));
    assertThat(messages.getMessage(messages.getHelp(), new Dog(), "hierarchy-2"), equalTo(Optional.of("pass-dog")));
    assertThat(messages.getMessage(messages.getHelp(), new Dog(), "hierarchy-3"), equalTo(Optional.empty()));
    // these are basically the same as the above
    assertThat(messages.getMessage(messages.getHelp(), Dog.class, "hierarchy-1"), equalTo(Optional.of("pass-animal")));
    assertThat(messages.getMessage(messages.getHelp(), Dog.class, "hierarchy-2"), equalTo(Optional.of("pass-dog")));
    assertThat(messages.getMessage(messages.getHelp(), Dog.class, "hierarchy-3"), equalTo(Optional.empty()));
  }

  @Test
  public void getDetailedMessageFindsAlltheMessages() throws Exception {
    // when help spans multiple paragraphs, it gets returned as a list
    help.bundle.put("IdentifiedFunction.by-id.foo.bar", "brief");
    help.bundle.put("IdentifiedFunction.by-id.foo.bar.1", "one");
    help.bundle.put("IdentifiedFunction.by-id.foo.bar.2", "two");
    help.bundle.put("IdentifiedFunction.by-id.foo.bar.100", "not this");

    List<String> expected = Arrays.asList("brief", "one", "two");
    assertEquals(expected, messages.getDetailedMessage(messages.getHelp(), function, "bar"));
  }

  @RequiredArgsConstructor
  static class Code implements ProblemCode {

    private final String name;

    @Override
    public String name() {
      return name;
    }

    @Override
    public String toKey() {
      return name;
    }
  }
  @Test
  public void renderProblemTranslatesAndConvertsToRenderedProblem() throws Exception {
    Object toRender = new Object();
    // simple problem
    problems.bundle.put("foo", "foo error");
    Problem simple = Problem.error(new Code("foo"));

    // problem with args
    problems.bundle.put("with-args", "the thing {0} is bad");
    Problem stringArg = Problem.error(new Code("with-args"), "string arg");
    Problem renderedArg = Problem.error(new Code("with-args"), toRender);


    Mockito.when(mockDefaultRenderer.render(any(), Mockito.eq("string arg"), Mockito.any()))
      .thenReturn(Optional.of("string-arg"));
    Mockito.when(mockDefaultRenderer.render(Mockito.any(), Mockito.same(toRender), Mockito.any()))
      .thenReturn(Optional.of("rendered arg"));

    assertThat(rend(simple), equalTo(new RenderedProblem("foo error", simple)));
    assertThat(rend(stringArg), equalTo(new RenderedProblem("the thing string-arg is bad", stringArg)));
    assertThat(rend(renderedArg), equalTo(new RenderedProblem("the thing rendered arg is bad", renderedArg)));

    // nested problem - simpler to assert this with the toString method
    Problem nested = simple.withChildren(stringArg.withChildren(renderedArg));
    assertThat(rend(nested).toString(), equalTo(""
        + "foo error\n"
        + "  - the thing string-arg is bad\n"
        + "    - the thing rendered arg is bad"
    ));

    // problem with no translation
    assertThat(rend(Problem.error(new Code("wut"), "foo", "baz")).message, equalTo("ProblemCode.wut: args=[foo, baz]"));
  }

  @Test
  public void pluginsCanAddCustomRenderingFunctions() throws Exception {
    extensionPoints.addExtensionPoint(ObjectRenderer.class);

    // nothing so far
    assertThat(messages.renderObject(MessagesTest.class, locale), nullValue());
    // mockDefaultRenderer was asked to render it
    verify(this.mockDefaultRenderer, times(1)).render(messages, MessagesTest.class, locale);

    // set up mocking on the default renderer to return something
    ObjectRenderer mockRenderer = Mockito.mock(ObjectRenderer.class);
    when(mockRenderer.render(messages, MessagesTest.class, locale)).thenReturn(Optional.of("Cool Story"));
    extensionPoints.addFeature(mockRenderer);

    // try again with the custom renderer
    assertThat(messages.renderObject(MessagesTest.class, locale), equalTo("Cool Story"));
    // confirm it didn't ask the default renderer for its opinion (we've got the same times as before)
    verify(this.mockDefaultRenderer, times(1)).render(messages, MessagesTest.class, locale);
  }

  public RenderedProblem rend(Problem problem) throws Exception {
    return messages.renderProblem(problem, Locale.getDefault());
  }
}
