package org.jabref.model.entry;

import java.util.List;
import java.util.stream.Stream;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

class KeywordListTest {

    private KeywordList keywords;

    @BeforeEach
    void setUp() {
        keywords = new KeywordList();
        keywords.add("keywordOne");
        keywords.add("keywordTwo");
    }

    @Test
    void parseEmptyStringReturnsEmptyList() {
        assertEquals(new KeywordList(), KeywordList.parse("", ','));
    }

    @Test
    void parseOneWordReturnsOneKeyword() {
        assertEquals(new KeywordList("keywordOne"),
                KeywordList.parse("keywordOne", ','));
    }

    @Test
    void parseTwoWordReturnsTwoKeywords() {
        assertEquals(new KeywordList("keywordOne", "keywordTwo"),
                KeywordList.parse("keywordOne, keywordTwo", ','));
    }

    @Test
    void parseTwoWordReturnsTwoKeywordsWithoutSpace() {
        assertEquals(new KeywordList("keywordOne", "keywordTwo"),
                KeywordList.parse("keywordOne,keywordTwo", ','));
    }

    @Test
    void parseTwoWordReturnsTwoKeywordsWithDifferentDelimiter() {
        assertEquals(new KeywordList("keywordOne", "keywordTwo"),
                KeywordList.parse("keywordOne| keywordTwo", '|'));
    }

    @Test
    void parseWordsWithWhitespaceReturnsOneKeyword() {
        assertEquals(new KeywordList("keyword and one"),
                KeywordList.parse("keyword and one", ','));
    }

    @Test
    void parseWordsWithWhitespaceAndCommaReturnsTwoKeyword() {
        assertEquals(new KeywordList("keyword and one", "and two"),
                KeywordList.parse("keyword and one, and two", ','));
    }

    @Test
    void parseIgnoresDuplicates() {
        assertEquals(new KeywordList("keywordOne", "keywordTwo"),
                KeywordList.parse("keywordOne, keywordTwo, keywordOne", ','));
    }

    @Test
    void parseTakeDelimiterNotRegexWhite() {
        assertEquals(new KeywordList("keywordOne keywordTwo", "keywordThree"),
                KeywordList.parse("keywordOne keywordTwoskeywordThree", 's'));
    }

    @Test
    void parseWordsWithBracketsReturnsOneKeyword() {
        assertEquals(new KeywordList("[a] keyword"), KeywordList.parse("[a] keyword", ','));
    }

    @Test
    void asStringAddsSpaceAfterDelimiter() {
        assertEquals("keywordOne, keywordTwo", keywords.getAsString(','));
    }

    @Test
    void parseWithMultipleDelimitersSingleKeyword() {
        assertEquals(new KeywordList("keywordOne"),
                KeywordList.parseWithMultipleDelimiters("keywordOne", List.of(';', ',')));
    }

    @Test
    void parseWithMultipleDelimitersWithSemicolonDelimiter() {
        assertEquals(new KeywordList("keywordOne", "keywordTwo"),
                KeywordList.parseWithMultipleDelimiters("keywordOne; keywordTwo", List.of(';', ',')));
    }

    @Test
    void parseWithMultipleDelimitersWithCommaDelimiter() {
        assertEquals(new KeywordList("keywordOne", "keywordTwo"),
                KeywordList.parseWithMultipleDelimiters("keywordOne, keywordTwo", List.of(';', ',')));
    }

    @Test
    void parseWithMultipleDelimitersSplitsOnAllConfiguredDelimiters() {
        assertEquals(new KeywordList("keywordOne", "keywordTwo", "keywordThree"),
                KeywordList.parseWithMultipleDelimiters("keywordOne, keywordTwo; keywordThree", List.of(';', ',')));
    }

    @Test
    void parseWithMultipleDelimitersDoesNotSplitConfiguredDelimitersInsideBraces() {
        assertEquals(new KeywordList("test1", "{2,1}", "test3"),
                KeywordList.parseWithMultipleDelimiters("test1; {2,1}; test3", List.of(';', ',')));
    }

    @Test
    void parseWithMultipleDelimitersDoesNotSplitEscapedConfiguredDelimiters() {
        assertEquals(new KeywordList("keyword#one", "keywordTwo"),
                KeywordList.parseWithMultipleDelimiters("keyword\\#one; keywordTwo", List.of(';', '#')));
    }

    @Test
    void parseHierarchicalChain() {
        Keyword expected = Keyword.of(List.of("Parent", "Node", "Child"));

        assertEquals(new KeywordList(expected), KeywordList.parse("Parent > Node > Child", ','));
    }

    @Test
    void parseWithMultipleDelimitersHierarchicalChain() {
        Keyword expected = Keyword.of(List.of("Parent", "Node", "Child"));

        assertEquals(new KeywordList(expected), KeywordList.parseWithMultipleDelimiters("Parent > Node > Child", List.of(';', ',')));
    }

    @Test
    void parseTwoHierarchicalChains() {
        Keyword expectedOne = Keyword.of(List.of("Parent1", "Node1", "Child1"));
        Keyword expectedTwo = Keyword.of(List.of("Parent2", "Node2", "Child2"));

        assertEquals(new KeywordList(expectedOne, expectedTwo),
                KeywordList.parse("Parent1 > Node1 > Child1, Parent2 > Node2 > Child2", ','));
    }

    @Test
    void mergeTwoIdenticalKeywordsShouldReturnOnKeyword() {
        assertEquals(new KeywordList("JabRef"), KeywordList.merge("JabRef", "JabRef", ','));
    }

    @Test
    void mergeOneEmptyKeywordAnAnotherNonEmptyShouldReturnTheNonEmptyKeyword() {
        assertEquals(new KeywordList("JabRef"), KeywordList.merge("", "JabRef", ','));
    }

    @Test
    void mergeTwoDistinctKeywordsShouldReturnTheTwoKeywordsMerged() {
        assertEquals(new KeywordList("Figma", "JabRef"), KeywordList.merge("Figma", "JabRef", ','));
        assertEquals(new KeywordList("JabRef", "Figma"), KeywordList.merge("Figma", "JabRef", ','));
    }

    @Test
    void mergeTwoListsOfKeywordsShouldReturnTheKeywordsMerged() {
        assertEquals(new KeywordList("Figma", "Adobe", "JabRef", "Eclipse", "JetBrains"), KeywordList.merge("Figma, Adobe, JetBrains, Eclipse", "Adobe, JabRef", ','));
    }

    private static Stream<Arguments> parseKeywordWithEscapedDelimiterDoesNotSplitKeyword() {
        return Stream.of(
                Arguments.of("keyword\\,one, keywordTwo", new KeywordList("keyword,one", "keywordTwo")),
                Arguments.of("keywordOne\\,, keywordTwo", new KeywordList("keywordOne,", "keywordTwo")),
                Arguments.of("keyword\\\\, keywordTwo", new KeywordList("keyword\\", "keywordTwo")),
                Arguments.of("keyword\\,one > sub", new KeywordList(Keyword.of(List.of("keyword,one", "sub")))),
                Arguments.of("one\\,two\\,three, four", new KeywordList("one,two,three", "four")),
                Arguments.of("keywordOne\\\\", new KeywordList("keywordOne\\"))
        );
    }

    @ParameterizedTest
    @MethodSource()
    void parseKeywordWithEscapedDelimiterDoesNotSplitKeyword(String input, KeywordList expected) {
        assertEquals(expected, KeywordList.parse(input, ','));
    }

    private static Stream<Arguments> serializeKeywordWithNonEscapedDelimiterJoinsKeywordsCorrectly() {
        return Stream.of(
                Arguments.of(List.of(new Keyword("keyword,one"), new Keyword("keywordTwo")), "keyword\\,one,keywordTwo"),
                Arguments.of(List.of(new Keyword("keywordOne,"), new Keyword("keywordTwo")), "keywordOne\\,,keywordTwo"),
                Arguments.of(List.of(Keyword.of(List.of("keyword\\")), Keyword.of(List.of("keywordTwo"))), "keyword\\\\,keywordTwo"),
                Arguments.of(List.of(Keyword.of(List.of("keyword,one", "sub"))), "keyword\\,one > sub"),
                Arguments.of(List.of(new Keyword("one,two,three"), new Keyword("four")), "one\\,two\\,three,four"),
                Arguments.of(List.of(new Keyword("keywordOne\\")), "keywordOne\\\\")
        );
    }

    @ParameterizedTest
    @MethodSource()
    void serializeKeywordWithNonEscapedDelimiterJoinsKeywordsCorrectly(List<Keyword> input, String expected) {
        assertEquals(expected, KeywordList.serialize(input, ','));
    }

    @ParameterizedTest
    @MethodSource("parseKeywordWithEscapedDelimiterDoesNotSplitKeyword")
    void afterFirstParsingNoChangesShouldBeDoneToKeywords(String input, KeywordList expected) {
        char delimiter = ',';
        KeywordList firstParse = KeywordList.parse(input, delimiter);
        String serialized = KeywordList.serialize(firstParse.stream().toList(), delimiter);
        KeywordList secondParse = KeywordList.parse(serialized, delimiter);
        assertEquals(expected, secondParse);
    }

    @ParameterizedTest
    @MethodSource("serializeKeywordWithNonEscapedDelimiterJoinsKeywordsCorrectly")
    void afterFirstSerializeNoChangesShouldBeDoneToKeywords(List<Keyword> input, String expected) {
        char delimiter = ',';
        String firstSerialize = KeywordList.serialize(input, delimiter);
        KeywordList parsed = KeywordList.parse(firstSerialize, delimiter);
        String secondSerialize = KeywordList.serialize(parsed.stream().toList(), delimiter);
        assertEquals(expected, secondSerialize);
    }

    @Test
    void serializeWithSpacesEscapesEmbeddedDelimiterAndKeepsReadableSeparatorSpacing() {
        assertEquals("keyword\\,one, keywordTwo",
                KeywordList.serializeWithSpaces(List.of(new Keyword("keyword,one"), new Keyword("keywordTwo")), ','));
    }
}
