/*
 * Copyright 2015, 2024 StreamEx contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package one.util.streamex.api;

import one.util.streamex.IntStreamEx;
import one.util.streamex.Joining;
import one.util.streamex.StreamEx;
import org.junit.Assert;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static one.util.streamex.TestHelpers.*;
import static org.junit.Assert.assertEquals;

/**
 * @author Tagir Valeev
 */
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class JoiningTest {
    @Test(expected = IllegalArgumentException.class)
    public void testMaxCharsRange() {
        Joining.with(",").maxChars(-1);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testMaxCodePointsRange() {
        Joining.with(",").maxCodePoints(-2);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testMaxSymbolsRange() {
        Joining.with(",").maxGraphemes(Integer.MIN_VALUE);
    }

    @Test
    public void testSimple() {
        Supplier<Stream<String>> s = () -> IntStream.range(0, 100).mapToObj(String::valueOf);
        checkCollector("joiningSimple", s.get().collect(Collectors.joining(", ")), s, Joining.with(", "));
        checkCollector("joiningWrap", s.get().collect(Collectors.joining(", ", "[", "]")), s, Joining.with(", ").wrap(
            "[", "]"));
        checkCollector("joiningWrap2", s.get().collect(Collectors.joining(", ", "[(", ")]")), s, Joining.with(", ")
                .wrap("(", ")").wrap("[", "]"));
    }

    @Test
    public void testCutSimple() {
        List<String> input = Arrays.asList("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
            "ten");
        assertEquals("", StreamEx.of(input).peek(Assert::fail).collect(Joining.with(", ").maxChars(0).cutAnywhere()));
        assertEquals("", StreamEx.of(input).parallel().peek(Assert::fail).collect(
            Joining.with(", ").maxChars(0).cutAnywhere()));
        String expected = "one, two, three, four, five, six, seven, eight, nine, ten";
        for (int i = 3; i < expected.length() + 5; i++) {
            String exp = expected;
            if (exp.length() > i) {
                exp = exp.substring(0, i - 3) + "...";
            }
            String exp2 = expected;
            while (exp2.length() > i) {
                int pos = exp2.lastIndexOf(", ", exp2.endsWith(", ...") ? exp2.length() - 6 : exp2.length());
                exp2 = pos >= 0 ? exp2.substring(0, pos + 2) + "..." : "...";
            }
            for (StreamExSupplier<String> supplier : streamEx(input::stream)) {
                assertEquals(supplier + "/#" + i, exp, supplier.get().collect(
                    Joining.with(", ").maxChars(i).cutAnywhere()));
                assertEquals(supplier + "/#" + i, expected.substring(0, Math.min(i, expected.length())), supplier.get()
                        .collect(Joining.with(", ").ellipsis("").maxChars(i).cutAnywhere()));
                assertEquals(supplier + "/#" + i, exp2, supplier.get().collect(
                    Joining.with(", ").maxChars(i).cutAfterDelimiter()));
            }
        }

        byte[] data = { (byte) 0xFF, 0x30, 0x40, 0x50, 0x70, 0x12, (byte) 0xF0 };
        assertEquals("FF 30 40 50 ...", IntStreamEx.of(data).mapToObj(
            b -> String.format(Locale.ENGLISH, "%02X", b & 0xFF)).collect(
            Joining.with(" ").maxChars(15).cutAfterDelimiter()));
    }

    @Test
    public void testCuts() {
        List<String> input = Arrays.asList("one two", "three four", "five", "six seven");

        checkShortCircuitCollector("cutBefore", "one two, three four...", 4, input::stream, Joining.with(", ")
                .maxChars(25).cutBeforeDelimiter());
        checkShortCircuitCollector("cutBefore", "one two...", 2, input::stream, Joining.with(", ").maxChars(10)
                .cutBeforeDelimiter());
        checkShortCircuitCollector("cutBefore", "...", 2, input::stream, Joining.with(", ").maxChars(9)
                .cutBeforeDelimiter());

        checkShortCircuitCollector("cutAfter", "one two, three four, ...", 4, input::stream, Joining.with(", ")
                .maxChars(25).cutAfterDelimiter());
        checkShortCircuitCollector("cutAfter", "one two, ...", 2, input::stream, Joining.with(", ").maxChars(12)
                .cutAfterDelimiter());
        checkShortCircuitCollector("cutAfter", "...", 2, input::stream, Joining.with(", ").maxChars(11)
                .cutAfterDelimiter());

        checkShortCircuitCollector("cutWord", "one two, three four, ...", 4, input::stream, Joining.with(", ")
                .maxChars(25).cutAtWord());
        checkShortCircuitCollector("cutWord", "one two, ...", 2, input::stream, Joining.with(", ").maxChars(12)
                .cutAtWord());
        checkShortCircuitCollector("cutWord", "one two,...", 2, input::stream, Joining.with(", ").maxChars(11)
                .cutAtWord());
        checkShortCircuitCollector("cutWord", "one two...", 2, input::stream, Joining.with(", ").maxChars(10)
                .cutAtWord());
        checkShortCircuitCollector("cutWord", "one ...", 2, input::stream, Joining.with(", ").maxChars(9).cutAtWord());
        checkShortCircuitCollector("cutWord", "one...", 1, input::stream, Joining.with(", ").maxChars(6).cutAtWord());

        checkShortCircuitCollector("cutCodePoint", "one two, three four, f...", 4, input::stream, Joining.with(", ")
                .maxChars(25));
        checkShortCircuitCollector("cutCodePoint", "one two, ...", 2, input::stream, Joining.with(", ").maxChars(12));
        checkShortCircuitCollector("cutCodePoint", "one two,...", 2, input::stream, Joining.with(", ").maxChars(11));
        checkShortCircuitCollector("cutCodePoint", "one two...", 2, input::stream, Joining.with(", ").maxChars(10));
        checkShortCircuitCollector("cutCodePoint", "one tw...", 2, input::stream, Joining.with(", ").maxChars(9));
        checkShortCircuitCollector("cutCodePoint", "one...", 1, input::stream, Joining.with(", ").maxChars(6));
    }

    @Test
    public void testPrefixSuffix() {
        List<String> input = Arrays.asList("one two", "three four", "five", "six seven");
        checkShortCircuitCollector("cutWord", "[one two, three four,...]", 3, input::stream, Joining.with(", ").wrap(
            "[", "]").maxChars(25).cutAtWord());
        checkShortCircuitCollector("cutWord", "[one two...]", 2, input::stream, Joining.with(", ").maxChars(12).wrap(
            "[", "]").cutAtWord());
        checkShortCircuitCollector("cutWord", "[one ...]", 2, input::stream, Joining.with(", ").maxChars(11).wrap("[",
            "]").cutAtWord());
        checkShortCircuitCollector("cutWord", "[one ...]", 2, input::stream, Joining.with(", ").maxChars(10).wrap("[",
            "]").cutAtWord());
        checkShortCircuitCollector("cutWord", "[one...]", 1, input::stream, Joining.with(", ").maxChars(8).wrap("[",
            "]").cutAtWord());
        checkShortCircuitCollector("cutWord", "[...]", 1, input::stream, Joining.with(", ").maxChars(6).wrap("[", "]")
                .cutAtWord());
        checkShortCircuitCollector("cutWord", "[..]", 1, input::stream, Joining.with(", ").maxChars(4).wrap("[", "]")
                .cutAtWord());
        checkShortCircuitCollector("cutWord", "[.]", 1, input::stream, Joining.with(", ").maxChars(3).wrap("[", "]")
                .cutAtWord());
        checkShortCircuitCollector("cutWord", "[]", 0, input::stream, Joining.with(", ").maxChars(2).wrap("[", "]")
                .cutAtWord());
        checkShortCircuitCollector("cutWord", "[", 0, input::stream, Joining.with(", ").maxChars(1).wrap("[", "]")
                .cutAtWord());
        checkShortCircuitCollector("cutWord", "", 0, input::stream, Joining.with(", ").maxChars(0).wrap("[", "]")
                .cutAtWord());

        checkShortCircuitCollector("cutWord", "a prefix  a ", 0, input::stream, Joining.with(" ").maxChars(15).wrap(
            "a prefix ", " a suffix").cutAtWord());
        checkShortCircuitCollector("cutWord", "a prefix  ", 0, input::stream, Joining.with(" ").maxChars(10).wrap(
            "a prefix ", " a suffix").cutAtWord());
        checkShortCircuitCollector("cutWord", "a ", 0, input::stream, Joining.with(" ").maxChars(5).wrap("a prefix ",
            " a suffix").cutAtWord());
    }

    @Test
    public void testCodePoints() {
        String string = "\ud801\udc14\ud801\udc2f\ud801\udc45\ud801\udc28\ud801\udc49\ud801\udc2f\ud801\udc3b";
        List<CharSequence> input = Arrays.asList(string, new StringBuilder(string), new StringBuffer(string));
        checkShortCircuitCollector("maxChars", "\ud801\udc14\ud801\udc2f\ud801", 1, input::stream, Joining.with(",")
                .ellipsis("").maxChars(5).cutAnywhere());
        checkShortCircuitCollector("maxChars", "\ud801\udc14\ud801\udc2f", 1, input::stream, Joining.with(",")
                .ellipsis("").maxChars(5).cutAtCodePoint());
        checkShortCircuitCollector("maxChars", "\ud801\udc14\ud801\udc2f", 1, input::stream, Joining.with(",")
                .ellipsis("").maxChars(4).cutAtGrapheme());
        checkShortCircuitCollector("maxChars", "\ud801\udc14\ud801\udc2f", 1, input::stream, Joining.with(",")
                .ellipsis("").maxChars(4).cutAnywhere());
        checkShortCircuitCollector("maxChars", "\ud801\udc14\ud801\udc2f", 1, input::stream, Joining.with(",")
                .ellipsis("").maxChars(4).cutAtCodePoint());
        checkShortCircuitCollector("maxChars", "\ud801\udc14\ud801\udc2f", 1, input::stream, Joining.with(",")
                .ellipsis("").maxChars(4).cutAtGrapheme());
        checkShortCircuitCollector("maxChars", "\ud801\udc14\ud801\udc2f\ud801\udc45\ud801\udc28", 1, input::stream,
            Joining.with(",").ellipsis("").maxChars(9));

        checkShortCircuitCollector("maxCodePoints", "\ud801\udc14\ud801\udc2f\ud801\udc45\ud801\udc28\ud801\udc49", 1,
            input::stream, Joining.with(",").ellipsis("").maxCodePoints(5).cutAnywhere());
        checkShortCircuitCollector("maxCodePoints", "\ud801\udc14\ud801\udc2f\ud801\udc45\ud801\udc28\ud801\udc49", 1,
            input::stream, Joining.with(",").ellipsis("").maxCodePoints(5).cutAtCodePoint());
        checkShortCircuitCollector("maxCodePoints", "\ud801\udc14\ud801\udc2f\ud801\udc45\ud801\udc28\ud801\udc49", 1,
            input::stream, Joining.with(",").ellipsis("").maxCodePoints(5).cutAtGrapheme());
        checkShortCircuitCollector("maxCodePoints", string, 2, input::stream, Joining.with(",").ellipsis("")
                .maxCodePoints(7));
        checkShortCircuitCollector("maxCodePoints", string + ",", 2, input::stream, Joining.with(",").ellipsis("")
                .maxCodePoints(8));
        checkShortCircuitCollector("maxCodePoints", string + ",\ud801\udc14", 2, input::stream, Joining.with(",")
                .ellipsis("").maxCodePoints(9).cutAtCodePoint());
        checkShortCircuitCollector("maxCodePoints", string + "," + string + "," + string, 3, input::stream, Joining
                .with(",").ellipsis("").maxCodePoints(23));
        checkShortCircuitCollector("maxCodePoints", string + "," + string
            + ",\ud801\udc14\ud801\udc2f\ud801\udc45\ud801\udc28\ud801\udc49\ud801\udc2f", 3, input::stream, Joining
                .with(",").ellipsis("").maxCodePoints(22));

        checkShortCircuitCollector("maxCodePointsPrefix", string, 0, input::stream, Joining.with(",").wrap(string,
            string).maxCodePoints(7).cutAnywhere());
    }

    @Test
    public void testSurrogates() {
        String string = "\ud801\ud801\ud801\ud801\ud801\udc14\udc14\udc14\udc14\udc14";
        List<String> input = Collections.nCopies(3, string);

        checkShortCircuitCollector("highSurr", string.substring(0, 4), 1, input::stream, Joining.with(",").ellipsis("")
                .maxChars(4).cutAnywhere());
        checkShortCircuitCollector("highSurr", string.substring(0, 4), 1, input::stream, Joining.with(",").ellipsis("")
                .maxChars(4).cutAtCodePoint());
        checkShortCircuitCollector("highSurr", string.substring(0, 4), 1, input::stream, Joining.with(",").ellipsis("")
                .maxCodePoints(4).cutAnywhere());

        checkShortCircuitCollector("lowSurr", string.substring(0, 7), 1, input::stream, Joining.with(",").ellipsis("")
                .maxChars(7).cutAnywhere());
        checkShortCircuitCollector("lowSurr", string.substring(0, 7), 1, input::stream, Joining.with(",").ellipsis("")
                .maxChars(7).cutAtCodePoint());
        checkShortCircuitCollector("lowSurr", string.substring(0, 8), 1, input::stream, Joining.with(",").ellipsis("")
                .maxCodePoints(7).cutAnywhere());

    }

    @Test
    public void testGraphemes() {
        String string = "aa\u0300\u0321e\u0300a\u0321a\u0300\u0321a";
        List<String> input = Collections.nCopies(3, string);
        checkShortCircuitCollector("maxChars", "aa\u0300\u0321e", 1, input::stream, Joining.with(",").ellipsis("")
                .maxChars(5).cutAnywhere());
        checkShortCircuitCollector("maxChars", "aa\u0300\u0321e", 1, input::stream, Joining.with(",").ellipsis("")
                .maxChars(5).cutAtCodePoint());
        checkShortCircuitCollector("maxChars", "aa\u0300\u0321", 1, input::stream, Joining.with(",").ellipsis("")
                .maxChars(5).cutAtGrapheme());
        checkShortCircuitCollector("maxChars", "aa\u0300\u0321e\u0300", 1, input::stream, Joining.with(",")
                .ellipsis("").maxChars(6).cutAtGrapheme());
        checkShortCircuitCollector("maxChars", "aa\u0300\u0321", 1, input::stream, Joining.with(",").ellipsis("")
                .maxChars(4).cutAtGrapheme());
        checkShortCircuitCollector("maxChars", "a", 1, input::stream, Joining.with(",").ellipsis("").maxChars(3)
                .cutAtGrapheme());

        checkShortCircuitCollector("maxSymbols", "aa\u0300\u0321e\u0300", 1, input::stream, Joining.with(",").ellipsis(
            "").maxGraphemes(3));
        checkShortCircuitCollector("maxSymbols", "aa\u0300\u0321e\u0300a\u0321a\u0300\u0321", 1, input::stream, Joining
                .with(",").ellipsis("").maxGraphemes(5));
        checkShortCircuitCollector("maxSymbols", string, 2, input::stream, Joining.with(",").ellipsis("").maxGraphemes(
            6));
        checkShortCircuitCollector("maxSymbols", string + ",", 2, input::stream, Joining.with(",").ellipsis("")
                .maxGraphemes(7));
        checkShortCircuitCollector("maxSymbols", string + ",a", 2, input::stream, Joining.with(",").ellipsis("")
                .maxGraphemes(8));
        checkShortCircuitCollector("maxSymbols", string + ",aa\u0300\u0321", 2, input::stream, Joining.with(",")
                .ellipsis("").maxGraphemes(9));

        checkShortCircuitCollector("maxSymbolsBeforeDelimiter", "", 1, input::stream, Joining.with(",").ellipsis("")
                .maxGraphemes(5).cutBeforeDelimiter());
        checkShortCircuitCollector("maxSymbolsBeforeDelimiter", string, 2, input::stream, Joining.with(",")
                .ellipsis("").maxGraphemes(6).cutBeforeDelimiter());
        checkShortCircuitCollector("maxSymbolsBeforeDelimiter", string, 2, input::stream, Joining.with(",")
                .ellipsis("").maxGraphemes(7).cutBeforeDelimiter());
        checkShortCircuitCollector("maxSymbolsBeforeDelimiter", string, 2, input::stream, Joining.with(",")
                .ellipsis("").maxGraphemes(8).cutBeforeDelimiter());

        checkShortCircuitCollector("maxSymbolsAfterDelimiter", "", 1, input::stream, Joining.with(",").ellipsis("")
                .maxGraphemes(5).cutAfterDelimiter());
        checkShortCircuitCollector("maxSymbolsAfterDelimiter", "", 2, input::stream, Joining.with(",").ellipsis("")
                .maxGraphemes(6).cutAfterDelimiter());
        checkShortCircuitCollector("maxSymbolsAfterDelimiter", string + ",", 2, input::stream, Joining.with(",")
                .ellipsis("").maxGraphemes(7).cutAfterDelimiter());
        checkShortCircuitCollector("maxSymbolsAfterDelimiter", string + ",", 2, input::stream, Joining.with(",")
                .ellipsis("").maxGraphemes(8).cutAfterDelimiter());

        checkShortCircuitCollector("maxSymbolsBeforeDelimiterPrefix", string, 0, input::stream, Joining.with(",").wrap(
            string, string).maxGraphemes(8).cutBeforeDelimiter());
    }

    @Test
    public void testMaxElements() {
        List<String> input = Arrays.asList("one", "two", "three", "four");
        checkShortCircuitCollector("maxElements", "", 0, StreamEx::empty, Joining.with(", ").maxElements(0));
        checkShortCircuitCollector("maxElements", "...", 1, input::stream, Joining.with(", ").maxElements(0));
        checkShortCircuitCollector("maxElements", "one, ...", 2, input::stream, Joining.with(", ").maxElements(1));
        checkShortCircuitCollector("maxElements", "one, two, ...", 3, input::stream, Joining.with(", ").maxElements(2));
        checkShortCircuitCollector("maxElements", "one, two, three, ...", 4, input::stream, Joining.with(", ").maxElements(3));
        checkShortCircuitCollector("maxElements", "one, two, three, four", 4, input::stream, Joining.with(", ").maxElements(4));

        checkShortCircuitCollector("maxElements", "...", 1, input::stream, Joining.with(", ").maxElements(0).cutBeforeDelimiter());
        checkShortCircuitCollector("maxElements", "one...", 2, input::stream, Joining.with(", ").maxElements(1).cutBeforeDelimiter());
        checkShortCircuitCollector("maxElements", "one, two...", 3, input::stream, Joining.with(", ").maxElements(2).cutBeforeDelimiter());
        checkShortCircuitCollector("maxElements", "one, two, three...", 4, input::stream, Joining.with(", ").maxElements(3).cutBeforeDelimiter());
        checkShortCircuitCollector("maxElements", "one, two, three, four", 4, input::stream, Joining.with(", ").maxElements(4).cutBeforeDelimiter());
    }
}
