// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
*******************************************************************************
*   Copyright (C) 2011-2012, International Business Machines
*   Corporation and others.  All Rights Reserved.
*******************************************************************************
*   created on: 2011aug12
*   created by: Markus W. Scherer
*/

package com.ibm.icu.dev.test.format;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.text.MessagePattern;
import com.ibm.icu.text.MessagePatternUtil;
import com.ibm.icu.text.MessagePatternUtil.ArgNode;
import com.ibm.icu.text.MessagePatternUtil.ComplexArgStyleNode;
import com.ibm.icu.text.MessagePatternUtil.MessageContentsNode;
import com.ibm.icu.text.MessagePatternUtil.MessageNode;
import com.ibm.icu.text.MessagePatternUtil.TextNode;
import com.ibm.icu.text.MessagePatternUtil.VariantNode;

/**
 * Test MessagePatternUtil (MessagePattern-as-tree-of-nodes API)
 * by building parallel trees of nodes and verifying that they match.
 */
@RunWith(JUnit4.class)
public final class MessagePatternUtilTest extends TestFmwk {
    // The following nested "Expect..." classes are used to build
    // a tree structure parallel to what the MessagePatternUtil class builds.
    // These nested test classes are not static so that they have access to TestFmwk methods.

    private class ExpectMessageNode {
        private ExpectMessageNode expectTextThatContains(String s) {
            contents.add(new ExpectTextNode(s));
            return this;
        }
        private ExpectMessageNode expectReplaceNumber() {
            contents.add(new ExpectMessageContentsNode());
            return this;
        }
        private ExpectMessageNode expectNoneArg(Object name) {
            contents.add(new ExpectArgNode(name));
            return this;
        }
        private ExpectMessageNode expectSimpleArg(Object name, String type) {
            contents.add(new ExpectArgNode(name, type));
            return this;
        }
        private ExpectMessageNode expectSimpleArg(Object name, String type, String style) {
            contents.add(new ExpectArgNode(name, type, style));
            return this;
        }
        private ExpectComplexArgNode expectChoiceArg(Object name) {
            return expectComplexArg(name, MessagePattern.ArgType.CHOICE);
        }
        private ExpectComplexArgNode expectPluralArg(Object name) {
            return expectComplexArg(name, MessagePattern.ArgType.PLURAL);
        }
        private ExpectComplexArgNode expectSelectArg(Object name) {
            return expectComplexArg(name, MessagePattern.ArgType.SELECT);
        }
        private ExpectComplexArgNode expectSelectOrdinalArg(Object name) {
            return expectComplexArg(name, MessagePattern.ArgType.SELECTORDINAL);
        }
        private ExpectComplexArgNode expectComplexArg(Object name, MessagePattern.ArgType argType) {
            ExpectComplexArgNode complexArg = new ExpectComplexArgNode(this, name, argType);
            contents.add(complexArg);
            return complexArg;
        }
        private ExpectComplexArgNode finishVariant() {
            return parent;
        }
        private void checkMatches(MessageNode msg) {
            // matches() prints all errors.
            matches(msg);
        }
        private boolean matches(MessageNode msg) {
            List<MessageContentsNode> msgContents = msg.getContents();
            boolean ok = assertEquals("different numbers of MessageContentsNode",
                                      contents.size(), msgContents.size());
            if (ok) {
                Iterator<MessageContentsNode> msgIter = msgContents.iterator();
                for (ExpectMessageContentsNode ec : contents) {
                    ok &= ec.matches(msgIter.next());
                }
            }
            if (!ok) {
                errln("error in message: " + msg.toString());
            }
            return ok;
        }
        private ExpectComplexArgNode parent;  // for finishVariant()
        private List<ExpectMessageContentsNode> contents =
            new ArrayList<ExpectMessageContentsNode>();
    }

    /**
     * Base class for message contents nodes.
     * Used directly for REPLACE_NUMBER nodes, subclassed for others.
     */
    private class ExpectMessageContentsNode {
        protected boolean matches(MessageContentsNode c) {
            return assertEquals("not a REPLACE_NUMBER node",
                                MessageContentsNode.Type.REPLACE_NUMBER, c.getType());
        }
    }

    private class ExpectTextNode extends ExpectMessageContentsNode {
        private ExpectTextNode(String subString) {
            this.subString = subString;
        }
        @Override
        protected boolean matches(MessageContentsNode c) {
            return
                assertEquals("not a TextNode",
                             MessageContentsNode.Type.TEXT, c.getType()) &&
                assertTrue("TextNode does not contain \"" + subString + "\"",
                           ((TextNode)c).getText().contains(subString));
        }
        private String subString;
    }

    private class ExpectArgNode extends ExpectMessageContentsNode {
        private ExpectArgNode(Object name) {
            this(name, null, null);
        }
        private ExpectArgNode(Object name, String type) {
            this(name, type, null);
        }
        private ExpectArgNode(Object name, String type, String style) {
            if (name instanceof String) {
                this.name = (String)name;
                this.number = -1;
            } else {
                this.number = (Integer)name;
                this.name = Integer.toString(this.number);
            }
            if (type == null) {
                argType = MessagePattern.ArgType.NONE;
            } else {
                argType = MessagePattern.ArgType.SIMPLE;
            }
            this.type = type;
            this.style = style;
        }
        @Override
        protected boolean matches(MessageContentsNode c) {
            boolean ok =
                assertEquals("not an ArgNode",
                             MessageContentsNode.Type.ARG, c.getType());
            if (!ok) {
                return ok;
            }
            ArgNode arg = (ArgNode)c;
            ok &= assertEquals("unexpected ArgNode argType",
                               argType, arg.getArgType());
            ok &= assertEquals("unexpected ArgNode arg name",
                               name, arg.getName());
            ok &= assertEquals("unexpected ArgNode arg number",
                               number, arg.getNumber());
            ok &= assertEquals("unexpected ArgNode arg type name",
                               type, arg.getTypeName());
            ok &= assertEquals("unexpected ArgNode arg style",
                               style, arg.getSimpleStyle());
            if (argType == MessagePattern.ArgType.NONE || argType == MessagePattern.ArgType.SIMPLE) {
                ok &= assertNull("unexpected non-null complex style", arg.getComplexStyle());
            }
            return ok;
        }
        private String name;
        private int number;
        protected MessagePattern.ArgType argType;
        private String type;
        private String style;
    }

    private class ExpectComplexArgNode extends ExpectArgNode {
        private ExpectComplexArgNode(ExpectMessageNode parent,
                                     Object name, MessagePattern.ArgType argType) {
            super(name, argType.toString().toLowerCase(Locale.ENGLISH));
            this.argType = argType;
            this.parent = parent;
        }
        private ExpectComplexArgNode expectOffset(double offset) {
            this.offset = offset;
            explicitOffset = true;
            return this;
        }
        private ExpectMessageNode expectVariant(String selector) {
            ExpectVariantNode variant = new ExpectVariantNode(this, selector);
            variants.add(variant);
            return variant.msg;
        }
        private ExpectMessageNode expectVariant(String selector, double value) {
            ExpectVariantNode variant = new ExpectVariantNode(this, selector, value);
            variants.add(variant);
            return variant.msg;
        }
        private ExpectMessageNode finishComplexArg() {
            return parent;
        }
        @Override
        protected boolean matches(MessageContentsNode c) {
            boolean ok = super.matches(c);
            if (!ok) {
                return ok;
            }
            ArgNode arg = (ArgNode)c;
            ComplexArgStyleNode complexStyle = arg.getComplexStyle();
            ok &= assertNotNull("unexpected null complex style", complexStyle);
            if (!ok) {
                return ok;
            }
            ok &= assertEquals("unexpected complex-style argType",
                               argType, complexStyle.getArgType());
            ok &= assertEquals("unexpected complex-style hasExplicitOffset()",
                               explicitOffset, complexStyle.hasExplicitOffset());
            ok &= assertEquals("unexpected complex-style offset",
                               offset, complexStyle.getOffset());
            List<VariantNode> complexVariants = complexStyle.getVariants();
            ok &= assertEquals("different number of variants",
                               variants.size(), complexVariants.size());
            if (!ok) {
                return ok;
            }
            Iterator<VariantNode> complexIter = complexVariants.iterator();
            for (ExpectVariantNode variant : variants) {
                ok &= variant.matches(complexIter.next());
            }
            return ok;
        }
        private ExpectMessageNode parent;  // for finishComplexArg()
        private boolean explicitOffset;
        private double offset;
        private List<ExpectVariantNode> variants = new ArrayList<ExpectVariantNode>();
    }

    private class ExpectVariantNode {
        private ExpectVariantNode(ExpectComplexArgNode parent, String selector) {
            this(parent, selector, MessagePattern.NO_NUMERIC_VALUE);
        }
        private ExpectVariantNode(ExpectComplexArgNode parent, String selector, double value) {
            this.selector = selector;
            numericValue = value;
            msg = new ExpectMessageNode();
            msg.parent = parent;
        }
        private boolean matches(VariantNode v) {
            boolean ok = assertEquals("different selector strings",
                                      selector, v.getSelector());
            ok &= assertEquals("different selector strings",
                               isSelectorNumeric(), v.isSelectorNumeric());
            ok &= assertEquals("different selector strings",
                               numericValue, v.getSelectorValue());
            return ok & msg.matches(v.getMessage());
        }
        private boolean isSelectorNumeric() {
            return numericValue != MessagePattern.NO_NUMERIC_VALUE;
        }
        private String selector;
        private double numericValue;
        private ExpectMessageNode msg;
    }

    // The actual tests start here. ---------------------------------------- ***
    // Sample message strings are mostly from the MessagePatternUtilDemo.

    @Test
    public void TestHello() {
        // No syntax.
        MessageNode msg = MessagePatternUtil.buildMessageNode("Hello!");
        ExpectMessageNode expect = new ExpectMessageNode().expectTextThatContains("Hello");
        expect.checkMatches(msg);
    }

    @Test
    public void TestHelloWithApos() {
        // Literal ASCII apostrophe.
        MessageNode msg = MessagePatternUtil.buildMessageNode("Hel'lo!");
        ExpectMessageNode expect = new ExpectMessageNode().expectTextThatContains("Hel'lo");
        expect.checkMatches(msg);
    }

    @Test
    public void TestHelloWithQuote() {
        // Apostrophe starts quoted literal text.
        MessageNode msg = MessagePatternUtil.buildMessageNode("Hel'{o!");
        ExpectMessageNode expect = new ExpectMessageNode().expectTextThatContains("Hel{o");
        expect.checkMatches(msg);
        // Terminating the quote should yield the same result.
        msg = MessagePatternUtil.buildMessageNode("Hel'{'o!");
        expect.checkMatches(msg);
        // Double apostrophe inside quoted literal text still encodes a single apostrophe.
        msg = MessagePatternUtil.buildMessageNode("a'{bc''de'f");
        expect = new ExpectMessageNode().expectTextThatContains("a{bc'def");
        expect.checkMatches(msg);
    }

    @Test
    public void TestNoneArg() {
        // Numbered argument.
        MessageNode msg = MessagePatternUtil.buildMessageNode("abc{0}def");
        ExpectMessageNode expect = new ExpectMessageNode().
            expectTextThatContains("abc").expectNoneArg(0).expectTextThatContains("def");
        expect.checkMatches(msg);
        // Named argument.
        msg = MessagePatternUtil.buildMessageNode("abc{ arg }def");
        expect = new ExpectMessageNode().
            expectTextThatContains("abc").expectNoneArg("arg").expectTextThatContains("def");
        expect.checkMatches(msg);
        // Numbered and named arguments.
        msg = MessagePatternUtil.buildMessageNode("abc{1}def{arg}ghi");
        expect = new ExpectMessageNode().
            expectTextThatContains("abc").expectNoneArg(1).expectTextThatContains("def").
            expectNoneArg("arg").expectTextThatContains("ghi");
        expect.checkMatches(msg);
    }

    @Test
    public void TestSimpleArg() {
        MessageNode msg = MessagePatternUtil.buildMessageNode("a'{bc''de'f{0,number,g'hi''jk'l#}");
        ExpectMessageNode expect = new ExpectMessageNode().
            expectTextThatContains("a{bc'def").expectSimpleArg(0, "number", "g'hi''jk'l#");
        expect.checkMatches(msg);
    }

    @Test
    public void TestSelectArg() {
        MessageNode msg = MessagePatternUtil.buildMessageNode(
                "abc{2, number}ghi{3, select, xx {xxx} other {ooo}} xyz");
        ExpectMessageNode expect = new ExpectMessageNode().
            expectTextThatContains("abc").expectSimpleArg(2, "number").
            expectTextThatContains("ghi").
            expectSelectArg(3).
                expectVariant("xx").expectTextThatContains("xxx").finishVariant().
                expectVariant("other").expectTextThatContains("ooo").finishVariant().
                finishComplexArg().
            expectTextThatContains(" xyz");
        expect.checkMatches(msg);
    }

    @Test
    public void TestPluralArg() {
        // Plural with only keywords.
        MessageNode msg = MessagePatternUtil.buildMessageNode(
                "abc{num_people, plural, offset:17 few{fff} other {oooo}}xyz");
        ExpectMessageNode expect = new ExpectMessageNode().
            expectTextThatContains("abc").
            expectPluralArg("num_people").
                expectOffset(17).
                expectVariant("few").expectTextThatContains("fff").finishVariant().
                expectVariant("other").expectTextThatContains("oooo").finishVariant().
                finishComplexArg().
            expectTextThatContains("xyz");
        expect.checkMatches(msg);
        // Plural with explicit-value selectors.
        msg = MessagePatternUtil.buildMessageNode(
                "abc{ num , plural , offset: 2 =1 {1} =-1 {-1} =3.14 {3.14} other {oo} }xyz");
        expect = new ExpectMessageNode().
            expectTextThatContains("abc").
            expectPluralArg("num").
                expectOffset(2).
                expectVariant("=1", 1).expectTextThatContains("1").finishVariant().
                expectVariant("=-1", -1).expectTextThatContains("-1").finishVariant().
                expectVariant("=3.14", 3.14).expectTextThatContains("3.14").finishVariant().
                expectVariant("other").expectTextThatContains("oo").finishVariant().
                finishComplexArg().
            expectTextThatContains("xyz");
        expect.checkMatches(msg);
        // Plural with number replacement.
        msg = MessagePatternUtil.buildMessageNode(
                "a_{0,plural,other{num=#'#'=#'#'={1,number,##}!}}_z");
        expect = new ExpectMessageNode().
            expectTextThatContains("a_").
            expectPluralArg(0).
                expectVariant("other").
                    expectTextThatContains("num=").expectReplaceNumber().
                    expectTextThatContains("#=").expectReplaceNumber().
                    expectTextThatContains("#=").expectSimpleArg(1, "number", "##").
                    expectTextThatContains("!").finishVariant().
                finishComplexArg().
            expectTextThatContains("_z");
        expect.checkMatches(msg);
        // Plural with explicit offset:0.
        msg = MessagePatternUtil.buildMessageNode(
                "a_{0,plural,offset:0 other{num=#!}}_z");
        expect = new ExpectMessageNode().
            expectTextThatContains("a_").
            expectPluralArg(0).
                expectOffset(0).
                expectVariant("other").
                    expectTextThatContains("num=").expectReplaceNumber().
                    expectTextThatContains("!").finishVariant().
                finishComplexArg().
            expectTextThatContains("_z");
        expect.checkMatches(msg);
    }


    @Test
    public void TestSelectOrdinalArg() {
        MessageNode msg = MessagePatternUtil.buildMessageNode(
                "abc{num, selectordinal, offset:17 =0{null} few{fff} other {oooo}}xyz");
        ExpectMessageNode expect = new ExpectMessageNode().
            expectTextThatContains("abc").
            expectSelectOrdinalArg("num").
                expectOffset(17).
                expectVariant("=0", 0).expectTextThatContains("null").finishVariant().
                expectVariant("few").expectTextThatContains("fff").finishVariant().
                expectVariant("other").expectTextThatContains("oooo").finishVariant().
                finishComplexArg().
            expectTextThatContains("xyz");
        expect.checkMatches(msg);
    }

    @Test
    public void TestChoiceArg() {
        MessageNode msg = MessagePatternUtil.buildMessageNode(
                "a_{0,choice,-∞ #-inf|  5≤ five | 99 # ninety'|'nine  }_z");
        ExpectMessageNode expect = new ExpectMessageNode().
            expectTextThatContains("a_").
            expectChoiceArg(0).
                expectVariant("#", Double.NEGATIVE_INFINITY).
                    expectTextThatContains("-inf").finishVariant().
                expectVariant("≤", 5).expectTextThatContains(" five ").finishVariant().
                expectVariant("#", 99).expectTextThatContains(" ninety|nine  ").finishVariant().
                finishComplexArg().
            expectTextThatContains("_z");
        expect.checkMatches(msg);
    }

    @Test
    public void TestComplexArgs() {
        MessageNode msg = MessagePatternUtil.buildMessageNode(
                "I don't {a,plural,other{w'{'on't #'#'}} and "+
                "{b,select,other{shan't'}'}} '{'''know'''}' and "+
                "{c,choice,0#can't'|'}"+
                "{z,number,#'#'###.00'}'}.");
        ExpectMessageNode expect = new ExpectMessageNode().
            expectTextThatContains("I don't ").
            expectPluralArg("a").
                expectVariant("other").
                    expectTextThatContains("w{on't ").expectReplaceNumber().
                    expectTextThatContains("#").finishVariant().
                finishComplexArg().
            expectTextThatContains(" and ").
            expectSelectArg("b").
                expectVariant("other").expectTextThatContains("shan't}").finishVariant().
                finishComplexArg().
            expectTextThatContains(" {'know'} and ").
            expectChoiceArg("c").
                expectVariant("#", 0).expectTextThatContains("can't|").finishVariant().
                finishComplexArg().
            expectSimpleArg("z", "number", "#'#'###.00'}'").
            expectTextThatContains(".");
        expect.checkMatches(msg);
    }

    /**
     * @return the text string of the VariantNode's message;
     *         assumes that its message consists of only text
     */
    private String variantText(VariantNode v) {
        return ((TextNode)v.getMessage().getContents().get(0)).getText();
    }

    @Test
    public void TestPluralVariantsByType() {
        MessageNode msg = MessagePatternUtil.buildMessageNode(
                "{p,plural,a{A}other{O}=4{iv}b{B}other{U}=2{ii}}");
        ExpectMessageNode expect = new ExpectMessageNode().
        expectPluralArg("p").
            expectVariant("a").expectTextThatContains("A").finishVariant().
            expectVariant("other").expectTextThatContains("O").finishVariant().
            expectVariant("=4", 4).expectTextThatContains("iv").finishVariant().
            expectVariant("b").expectTextThatContains("B").finishVariant().
            expectVariant("other").expectTextThatContains("U").finishVariant().
            expectVariant("=2", 2).expectTextThatContains("ii").finishVariant().
            finishComplexArg();
        if (!expect.matches(msg)) {
            return;
        }
        List<VariantNode> numericVariants = new ArrayList<VariantNode>();
        List<VariantNode> keywordVariants = new ArrayList<VariantNode>();
        VariantNode other =
            ((ArgNode)msg.getContents().get(0)).getComplexStyle().
            getVariantsByType(numericVariants, keywordVariants);
        assertEquals("'other' selector", "other", other.getSelector());
        assertEquals("message string of first 'other'", "O", variantText(other));

        assertEquals("numericVariants.size()", 2, numericVariants.size());
        VariantNode v = numericVariants.get(0);
        assertEquals("numericVariants[0] selector", "=4", v.getSelector());
        assertEquals("numericVariants[0] selector value", 4., v.getSelectorValue());
        assertEquals("numericVariants[0] text", "iv", variantText(v));
        v = numericVariants.get(1);
        assertEquals("numericVariants[1] selector", "=2", v.getSelector());
        assertEquals("numericVariants[1] selector value", 2., v.getSelectorValue());
        assertEquals("numericVariants[1] text", "ii", variantText(v));

        assertEquals("keywordVariants.size()", 2, keywordVariants.size());
        v = keywordVariants.get(0);
        assertEquals("keywordVariants[0] selector", "a", v.getSelector());
        assertFalse("keywordVariants[0].isSelectorNumeric()", v.isSelectorNumeric());
        assertEquals("keywordVariants[0] text", "A", variantText(v));
        v = keywordVariants.get(1);
        assertEquals("keywordVariants[1] selector", "b", v.getSelector());
        assertFalse("keywordVariants[1].isSelectorNumeric()", v.isSelectorNumeric());
        assertEquals("keywordVariants[1] text", "B", variantText(v));
    }

    @Test
    public void TestSelectVariantsByType() {
        MessageNode msg = MessagePatternUtil.buildMessageNode(
                "{s,select,a{A}other{O}b{B}other{U}}");
        ExpectMessageNode expect = new ExpectMessageNode().
        expectSelectArg("s").
            expectVariant("a").expectTextThatContains("A").finishVariant().
            expectVariant("other").expectTextThatContains("O").finishVariant().
            expectVariant("b").expectTextThatContains("B").finishVariant().
            expectVariant("other").expectTextThatContains("U").finishVariant().
            finishComplexArg();
        if (!expect.matches(msg)) {
            return;
        }
        // Check that we can use numericVariants = null.
        List<VariantNode> keywordVariants = new ArrayList<VariantNode>();
        VariantNode other =
            ((ArgNode)msg.getContents().get(0)).getComplexStyle().
            getVariantsByType(null, keywordVariants);
        assertEquals("'other' selector", "other", other.getSelector());
        assertEquals("message string of first 'other'", "O", variantText(other));

        assertEquals("keywordVariants.size()", 2, keywordVariants.size());
        VariantNode v = keywordVariants.get(0);
        assertEquals("keywordVariants[0] selector", "a", v.getSelector());
        assertFalse("keywordVariants[0].isSelectorNumeric()", v.isSelectorNumeric());
        assertEquals("keywordVariants[0] text", "A", variantText(v));
        v = keywordVariants.get(1);
        assertEquals("keywordVariants[1] selector", "b", v.getSelector());
        assertFalse("keywordVariants[1].isSelectorNumeric()", v.isSelectorNumeric());
        assertEquals("keywordVariants[1] text", "B", variantText(v));
    }
}
