/*******************************************************************************
 * Copyright (c) 2016, 2017 Pantheon Technologies, s.r.o. and others.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.eclipse.xtext.xbase.tests.lib;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import java.lang.reflect.Field;

import org.eclipse.xtend2.lib.StringConcatenation;
import org.eclipse.xtend2.lib.StringConcatenationClient;
import org.junit.Test;

public class StringConcatenationTest {
    // Utility class wrapping a String
    private static final class StringObject extends Object {
        final String string;

        StringObject(final String string) {
            this.string = string;
        }

        @Override
        public String toString() {
            return string;
        }
    }

    @Test
    public void testEmpty() {
        assertEquals("", new StringConcatenation().toString());
    }

    @Test(expected=IllegalArgumentException.class)
    public void testNullDelimiter() {
        new StringConcatenation(null);
    }

    @Test(expected=IllegalArgumentException.class)
    public void testEmptyDelimiter() {
        new StringConcatenation("");
    }

    @Test
    public void testAppendObjectNull() {
        final StringConcatenation c = new StringConcatenation();
        c.append((Object)null);
        c.append((Object)null, "");
        c.append((Object)null, " ");
        assertEquals("", c.toString());
    }

    @Test
    public void testAppendStringNull() {
        final StringConcatenation c = new StringConcatenation();
        c.append((String)null);
        c.append((String)null, "");
        c.append((String)null, " ");
        assertEquals("", c.toString());
    }

    @Test
    public void testAppendStringConcatNull() {
        final StringConcatenation c = new StringConcatenation();
        c.append((StringConcatenation)null);
        c.append((StringConcatenation)null, "");
        c.append((StringConcatenation)null, " ");
        assertEquals("", c.toString());
    }

    @Test
    public void testAppendStringConcatClientNull() {
        final StringConcatenation c = new StringConcatenation();
        c.append((StringConcatenationClient)null);
        c.append((StringConcatenationClient)null, "");
        c.append((StringConcatenationClient)null, " ");
        assertEquals("", c.toString());
    }

    @Test
    public void testAppendNullToString() {
        final Object o = new StringObject(null);
        final StringConcatenation c = new StringConcatenation();
        c.append(o);
        c.append(o, "");
        assertEquals("", new StringConcatenation().toString());
    }

    @Test(expected=NullPointerException.class)
    public void testAppendWithNullIndent() {
        final StringConcatenation c = new StringConcatenation();
        c.append("a", null);
    }

    @Test(expected=NullPointerException.class)
    public void testAppendNullWithNullIndent() {
        final StringConcatenation c = new StringConcatenation();
        c.append((Object)null, null);
    }

    @Test
    public void testStringConcat() {
        final StringConcatenation c = new StringConcatenation();
        c.append("a");
        c.append("b");
        c.append("c");
        assertEquals("abc", c.toString());
    }

    @Test
    public void testMaskedStringConcat() {
        final StringConcatenation c = new StringConcatenation();
        c.append((Object)"a");
        c.append((Object)"b");
        c.append((Object)"c");
        assertEquals("abc", c.toString());
    }

    @Test
    public void testMixedConcat() {
        final StringConcatenation c = new StringConcatenation();
        c.append("a");
        c.append(Character.valueOf('b'));
        c.append(new StringObject("c"));
        assertEquals("abc", c.toString());
    }

    @Test
    public void testCharSequenceMethods() {
        final StringConcatenation c = new StringConcatenation();
        c.append("a");
        c.append("b");
        c.append("c");

        assertEquals(3, c.length());
        assertEquals('a', c.charAt(0));
        assertEquals('b', c.charAt(1));
        assertEquals("ab", c.subSequence(0, 2));
        assertEquals("bc", c.subSequence(1, 3));
    }

    @Test
    public void testNoindentConcat() {
        final StringConcatenation c = new StringConcatenation("\n");
        c.append("a\n");
        c.append("b\r");
        c.append("c\n");
        assertEquals("a\nb\nc\n", c.toString());
    }

    @Test
    public void testIndentConcat() {
        final StringConcatenation c = new StringConcatenation("\n");
        c.append("a\n", " ");
        c.append("b\r", "  ");
        c.append("c\nd", "   ");
        assertEquals("a\n b\n  c\n   d", c.toString());
    }

    @Test
    public void testMaskedIndentConcat() {
        final StringConcatenation c = new StringConcatenation("\n");
        c.append((Object)"a\n", " ");
        c.append((Object)"b\r", "  ");
        c.append((Object)"c\nd", "   ");
        assertEquals("a\n b\n  c\n   d", c.toString());
    }

    @Test
    public void testObjectIndentConcat() {
        final StringConcatenation c = new StringConcatenation("\n");
        c.append(new StringObject("a\n"), " ");
        c.append(new StringObject("b\r"), "  ");
        c.append(new StringObject("c\nd"), "   ");
        assertEquals("a\n b\n  c\n   d", c.toString());
    }

    @Test
    public void testNewLine() {
        final StringConcatenation c = new StringConcatenation("\n");
        c.newLine();
        c.append("a");
        c.newLine();
        assertEquals("\na\n", c.toString());
    }

    @Test
    public void testNewLineIfNotEmpty() {
        final StringConcatenation c = new StringConcatenation("\n");
        c.newLineIfNotEmpty();
        c.append("a");
        c.newLineIfNotEmpty();
        c.append("b\n");
        c.newLineIfNotEmpty();
        c.append("  ");
        c.newLineIfNotEmpty();
        assertEquals("a\nb\n", c.toString());
    }

    @Test
    public void testCachedToString() {
        final StringConcatenation c = new StringConcatenation();
        c.append("a");
        c.append("b\n");

        final String str = c.toString();
        assertSame(str, c.toString());
    }

    @Test
    public void testAppendEmptyConcat() {
        final StringConcatenation c = new StringConcatenation();
        c.append(new StringConcatenation());
        assertEquals("", c.toString());
        c.append(new StringConcatenation(), "  ");
        assertEquals("", c.toString());
    }

    @Test
    public void testAppendConcat() {
        final StringConcatenation toAppend = new StringConcatenation("\n");
        toAppend.append("a\n");
        toAppend.append("b");

        final StringConcatenation c = new StringConcatenation("\n");
        c.append(toAppend);
        assertEquals("a\nb", c.toString());
    }

    @Test
    public void testAppendConcatSeparator() {
        final StringConcatenation toAppend = new StringConcatenation("\t");
        toAppend.append("a\t");
        toAppend.append("b");

        final StringConcatenation c = new StringConcatenation();
        c.append(toAppend);
        assertEquals("a\tb", c.toString());
    }

    @Test
    public void testAppendEmptyConcatSeparator() {
        final StringConcatenation c = new StringConcatenation();
        c.append(new StringConcatenation("\t"));
        assertEquals("", c.toString());
    }

    @Test
    public void testAppendMaskedConcat() {
        final StringConcatenation toAppend = new StringConcatenation("\n");
        toAppend.append("a\n");
        toAppend.append("b");

        final StringConcatenation c = new StringConcatenation("\n");
        c.append((Object)toAppend);
        assertEquals("a\nb", c.toString());
    }

    @Test
    public void testAppendMaskedIndentConcat() {
        final StringConcatenation toAppend = new StringConcatenation("\n");
        toAppend.append("a\n");
        toAppend.append("b");

        final StringConcatenation c = new StringConcatenation("\n");
        c.append((Object)toAppend, " ");
        assertEquals("a\n b", c.toString());
    }

    @Test
    public void testAppendImmediateSimple() {
        final StringConcatenation c = new StringConcatenation();
        c.appendImmediate("a", "  ");
        assertEquals("a", c.toString());
    }

    @Test
    public void testAppendImmediateComplex() {
        final StringConcatenation c = new StringConcatenation("\n");
        c.append("a\n");
        c.append(" b   ");
        c.newLineIfNotEmpty();
        c.append("    ");
        c.appendImmediate("c\nd", "  ");
        assertEquals("a\n b   c\n  d\n", c.toString());
    }

    @Test
    public void testGrowInChunks() throws Exception {
        // This is rather ugly, but gets the job done. It would be much cleaner if this test were in the same
        // package as the class being tested...
        final Field lss = StringConcatenation.class.getDeclaredField("lastSegmentsSize");
        lss.setAccessible(true);

        final StringConcatenation c = new StringConcatenation();
        final int initialSize = lss.getInt(c);
        assertTrue(initialSize >= 0);

        for (int i = 0; i < initialSize; ++i) {
            c.append("a");
        }

        // No reallocation should happen
        assertEquals(initialSize, lss.getInt(c));

        // This should trigger pre-allocation
        c.append("b");
        final int firstGrown = lss.getInt(c);
        assertTrue(firstGrown > initialSize);

        // The delta needs to be a power-of-two
        final int deltaSize = firstGrown - initialSize;
        assertEquals(1, Integer.bitCount(deltaSize));

        // Now just to make sure, append another set
        final StringConcatenation batch = new StringConcatenation();
        for (int i = 1; i < deltaSize * 2; ++i) {
            batch.newLine();
        }
        c.append(batch);

        final int secondGrown = lss.getInt(c);
        assertTrue(firstGrown < secondGrown);
        assertEquals(deltaSize, secondGrown - firstGrown);
    }
}