package completer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import org.junit.jupiter.api.Test;

import jline.console.completer.Completer;

import de.renew.console.completer.RenewArgumentCompleter;

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

class RenewArgumentCompleterTest {

    @Test
    void testConstructorWithDelimiterAndCompleters() {
        // given
        Completer completer1 = new TestCompleter();
        Completer completer2 = new TestCompleter();
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        RenewArgumentCompleter rac =
            new RenewArgumentCompleter(delimiter, Arrays.asList(completer1, completer2));

        // then
        assertEquals(2, rac.getCompleters().size());
        assertEquals(delimiter, rac.getDelimiter());
    }

    @Test
    void testConstructorWithCompleters() {
        // given
        Completer completer1 = new TestCompleter();
        Completer completer2 = new TestCompleter();

        // when
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer1, completer2);

        // then
        assertEquals(2, rac.getCompleters().size());
        assertInstanceOf(
            RenewArgumentCompleter.WhitespaceArgumentDelimiter.class, rac.getDelimiter());
    }

    // Complete tests
    @Test
    void testCompleteWithEmptyBuffer() {
        // given
        TestCompleter completer = new TestCompleter("test");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer);
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("", 0, candidates);

        // then
        assertEquals(-1, result);
        assertTrue(candidates.isEmpty());
    }

    @Test
    void testCompleteWithSingleArgument() {
        // given
        TestCompleter completer = new TestCompleter("test");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer);
        LinkedList<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("te", 2, candidates);

        // then
        assertEquals(0, result);
        assertEquals(1, candidates.size());
        assertEquals("test", candidates.getFirst());
    }

    @Test
    void testCompleteWithMultipleArguments() {
        // given
        TestCompleter completer1 = new TestCompleter("cmd2");
        TestCompleter completer2 = new TestCompleter("arg1");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer1, completer2);
        LinkedList<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("cmd2 a", 6, candidates);

        // then
        assertEquals(5, result);
        assertEquals(1, candidates.size());
        assertEquals("arg1", candidates.getFirst());
    }

    @Test
    void testCompleteWithCursorAtDelimiter() {
        // given
        TestCompleter completer = new TestCompleter("test");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer);
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("test ", 5, candidates);

        // then
        assertEquals(-1, result);
        assertTrue(candidates.isEmpty());
    }

    @Test
    void testCompleteWithMultipleDelimiters() {
        // given
        TestCompleter completer1 = new TestCompleter("cmd");
        TestCompleter completer2 = new TestCompleter("arg");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer1, completer2);
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("cmd  a", 6, candidates);

        // then
        assertEquals(5, result);
        assertEquals(1, candidates.size());
        assertEquals("arg", candidates.get(0));
    }

    @Test
    void testCompleteWithNullCandidates() {
        // given
        RenewArgumentCompleter rac = new RenewArgumentCompleter(new TestCompleter());

        // then
        assertThrows(IllegalArgumentException.class, () -> rac.complete("test", 4, null));
    }

    @Test
    void testCompleteWithStrictModeValidationValidInput() {
        // given
        TestCompleter completer1 = new TestCompleter("cmd1");
        TestCompleter completer2 = new TestCompleter("arg1");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer1, completer2);
        rac.setStrict(true);
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("cmd1 a", 6, candidates);

        // then
        assertEquals(5, result);
        assertEquals(1, candidates.size());
    }

    @Test
    void testCompleteWithStrictModeValidationInvalidInput() {
        // given
        TestCompleter completer1 = new TestCompleter("cmd1");
        TestCompleter completer2 = new TestCompleter("arg1");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer1, completer2);
        rac.setStrict(true);
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("invalid a", 9, candidates);

        // then
        assertEquals(-1, result);
        assertTrue(candidates.isEmpty());
    }

    @Test
    void testCompleteWithFullMatch() {
        // given
        TestCompleter completer = new TestCompleter("exactMatch");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer);
        LinkedList<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("exactMatch", 10, candidates);

        // then
        assertEquals(0, result);
        assertEquals(1, candidates.size());
        assertEquals("exactMatch", candidates.getFirst());
    }

    @Test
    void testCompleteWithInvalidArgumentIndex() {
        // given
        TestCompleter completer = new TestCompleter("cmd");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer);
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("cmd arg", 7, candidates);

        // then
        assertEquals(-1, result);
        assertTrue(candidates.isEmpty());
    }

    @Test
    void testCompleteWithFirstCompleterBehavior() {
        // given
        TestCompleter completer1 = new TestCompleter("first");
        TestCompleter completer2 = new TestCompleter("second");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer1, completer2);
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("fir", 3, candidates);

        // then
        assertEquals(0, result);
    }

    @Test
    void testCompleteWithSecondCompleterBehavior() {
        // given
        TestCompleter completer1 = new TestCompleter("first");
        TestCompleter completer2 = new TestCompleter("second");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer1, completer2);
        LinkedList<CharSequence> candidates = new LinkedList<>();
        // when
        int result = rac.complete("first sec", 9, candidates);

        // then
        assertEquals(6, result);
        assertEquals(1, candidates.size());
        assertEquals("second", candidates.getFirst());
    }

    @Test
    void testCompleteWithStrictModePartialMatches() {
        // given
        TestCompleter completer1 = new TestCompleter("exact");
        TestCompleter completer2 = new TestCompleter("match");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer1, completer2);
        rac.setStrict(true);
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("exa m", 5, candidates);

        // then
        assertEquals(-1, result);
    }

    @Test
    void testCompleteWithEmptyCompleterListAndEmptyInput() {
        // given
        RenewArgumentCompleter rac = new RenewArgumentCompleter();
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("", 0, candidates);

        // then
        assertEquals(-1, result);
        assertTrue(candidates.isEmpty());
    }

    @Test
    void testCompleteWithEmptyCompleterListAndNonEmptyInput() {
        // given
        RenewArgumentCompleter rac = new RenewArgumentCompleter();
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("someInput", 9, candidates);

        // then
        assertEquals(-1, result);
        assertTrue(candidates.isEmpty());
    }

    @Test
    void testAddCompleterAddsCompleterCorrectly() {
        // given
        TestCompleter completer1 = new TestCompleter();
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer1);

        // when
        TestCompleter completer2 = new TestCompleter();
        rac.addCompleter(completer2);

        // then
        assertEquals(2, rac.getCompleters().size());
    }

    @Test
    void testRemoveCompleterRemovesCompleterCorrectly() {
        // given
        TestCompleter completer1 = new TestCompleter();
        TestCompleter completer2 = new TestCompleter();
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer1);

        // when
        rac.addCompleter(completer2);
        rac.removeCompleter(completer1);

        // then
        assertEquals(1, rac.getCompleters().size());
        assertEquals(completer2, rac.getCompleters().get(0));
    }

    @Test
    void testArgumentListStoresArgumentsAndPositionsCorrectly() {
        // given
        String[] args = { "arg1", "arg2" };
        RenewArgumentCompleter.ArgumentList argList =
            new RenewArgumentCompleter.ArgumentList(args, 1, 2, 5);

        // then
        assertArrayEquals(args, argList.getArguments());
        assertEquals(1, argList.getCursorArgumentIndex());
        assertEquals(2, argList.getArgumentPosition());
        assertEquals(5, argList.getBufferPosition());
        assertEquals("arg2", argList.getCursorArgument());
    }

    @Test
    void testArgumentListAllowsModificationOfArgumentsAndPositions() {
        // given
        String[] args = { "arg1", "arg2" };
        RenewArgumentCompleter.ArgumentList argList =
            new RenewArgumentCompleter.ArgumentList(args, 1, 2, 5);

        // when
        argList.setArguments(new String[] { "new" });
        argList.setCursorArgumentIndex(0);
        argList.setArgumentPosition(1);
        argList.setBufferPosition(3);

        // then
        assertArrayEquals(new String[] { "new" }, argList.getArguments());
        assertEquals(0, argList.getCursorArgumentIndex());
        assertEquals(1, argList.getArgumentPosition());
        assertEquals(3, argList.getBufferPosition());
    }

    @Test
    void testArgumentListWithEmptyArguments() {
        // given
        RenewArgumentCompleter.ArgumentList argList1 =
            new RenewArgumentCompleter.ArgumentList(new String[] { }, 0, 0, 0);

        // then
        assertNull(argList1.getCursorArgument());
    }

    @Test
    void testArgumentListWithOutOfBoundsIndex() {
        // given
        RenewArgumentCompleter.ArgumentList argList2 =
            new RenewArgumentCompleter.ArgumentList(new String[] { "a" }, 1, 0, 0);

        // then
        assertNull(argList2.getCursorArgument());
    }

    @Test
    void testArgumentListWithNullArguments() {
        // given
        RenewArgumentCompleter.ArgumentList argList =
            new RenewArgumentCompleter.ArgumentList(new String[] { null }, 0, 0, 0);

        // then
        assertNull(argList.getCursorArgument());
    }

    @Test
    void testDelimitSplitsArgumentsByWhitespace() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        RenewArgumentCompleter.ArgumentList result = delimiter.delimit("cmd arg1 arg2", 5);

        // then
        assertArrayEquals(new String[] { "cmd", "arg1", "arg2" }, result.getArguments());
        assertEquals(1, result.getCursorArgumentIndex());
        assertEquals(1, result.getArgumentPosition());
        assertEquals(5, result.getBufferPosition());
    }

    @Test
    void testDelimitEmptyString() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        RenewArgumentCompleter.ArgumentList result = delimiter.delimit("", 0);

        // then
        assertArrayEquals(new String[] { }, result.getArguments());
        assertEquals(0, result.getCursorArgumentIndex());
        assertEquals(0, result.getArgumentPosition());
        assertEquals(0, result.getBufferPosition());
    }

    @Test
    void testDelimitNullInput() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        RenewArgumentCompleter.ArgumentList result = delimiter.delimit(null, 0);

        // then
        assertArrayEquals(new String[] { }, result.getArguments());
        assertEquals(-1, result.getCursorArgumentIndex());
        assertEquals(-1, result.getArgumentPosition());
        assertEquals(0, result.getBufferPosition());
    }

    @Test
    void testDelimitSingleArgument() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        RenewArgumentCompleter.ArgumentList result = delimiter.delimit("command", 4);

        // then
        assertArrayEquals(new String[] { "command" }, result.getArguments());
        assertEquals(0, result.getCursorArgumentIndex());
        assertEquals(4, result.getArgumentPosition());
    }

    @Test
    void testIsDelimiterCharDetectsWhitespaceCorrectly() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        boolean resultSpace = delimiter.isDelimiterChar("a b", 1);
        boolean resultNotSpace = delimiter.isDelimiterChar("a b", 0);
        boolean resultTab = delimiter.isDelimiterChar("a\tb", 1);
        boolean resultNewline = delimiter.isDelimiterChar("a\nb", 1);

        // then
        assertTrue(resultSpace);
        assertFalse(resultNotSpace);
        assertTrue(resultTab);
        assertTrue(resultNewline);
    }

    @Test
    void testIsDelimiterDetectsWhitespaceCorrectly() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();
        CharSequence buffer = "arg1 'quoted arg' arg2\\ with\\ escape";

        // when
        boolean resultSpace = delimiter.isDelimiter(buffer, 4);
        boolean resultNotSpace = delimiter.isDelimiter(buffer, 0);

        // then
        assertTrue(resultSpace);
        assertFalse(resultNotSpace);
    }

    @Test
    void testConstructorWithCompleterList() {
        // given
        List<Completer> completers = new ArrayList<>();
        completers.add(new TestCompleter("cmd1"));
        completers.add(new TestCompleter("cmd2"));

        // when
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completers);

        // then
        assertEquals(2, rac.getCompleters().size());
        assertInstanceOf(
            RenewArgumentCompleter.WhitespaceArgumentDelimiter.class, rac.getDelimiter());
    }

    @Test
    void testCompleteWithNullBuffer() {
        // given
        TestCompleter completer = new TestCompleter("test");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer);
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete(null, 0, candidates);

        // then
        assertEquals(-1, result);
    }

    @Test
    void testCompleteStrictModeFailsIfPreviousCompleterHasNoCandidates() {
        // given
        Completer emptyCompleter = (buffer, cursor, candidates) -> 0;
        Completer dummyCompleter = new TestCompleter("dummy");

        RenewArgumentCompleter rac = new RenewArgumentCompleter(emptyCompleter, dummyCompleter);
        rac.setStrict(true);
        List<CharSequence> candidates = new LinkedList<>();

        // when
        int result = rac.complete("arg1 arg2", 9, candidates);

        // then
        assertEquals(-1, result);
        assertTrue(candidates.isEmpty());
    }

    @Test
    void testCompleteTrimsTrailingWhitespaceFromCandidates() {
        // given
        Completer completer = (buffer, cursor, candidates) -> {
            candidates.add("value1 ");
            candidates.add("value2\t");
            return 0;
        };

        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer);
        LinkedList<CharSequence> candidates = new LinkedList<>();
        String buffer = "cmd ";
        int cursor = 3;

        // when
        int result = rac.complete(buffer, cursor, candidates);

        // then
        assertEquals(0, result);
        assertEquals(2, candidates.size());
        assertEquals("value1", candidates.getFirst().toString());
        assertEquals("value2", candidates.get(1).toString());
    }

    @Test
    void testGetAndSetQuoteChars() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();
        char[] initialQuoteChars = new char[] { '\'', '"' };
        char[] newQuoteChars = new char[] { '`', '|' };

        // when
        char[] beforeChange = delimiter.getQuoteChars();
        delimiter.setQuoteChars(newQuoteChars);
        char[] afterChange = delimiter.getQuoteChars();

        // then
        assertArrayEquals(initialQuoteChars, beforeChange);
        assertArrayEquals(newQuoteChars, afterChange);
    }


    @Test
    void testGetAndSetEscapeChars() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();
        char[] initialEscapeChars = new char[] { '\\' };
        char[] newEscapeChars = new char[] { '$', '^' };

        // when
        char[] beforeChange = delimiter.getEscapeChars();
        delimiter.setEscapeChars(newEscapeChars);
        char[] afterChange = delimiter.getEscapeChars();

        // then
        assertArrayEquals(initialEscapeChars, beforeChange);
        assertArrayEquals(newEscapeChars, afterChange);
    }

    @Test
    void testSetQuoteCharsWithNull() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        delimiter.setQuoteChars(null);

        // then
        assertNull(delimiter.getQuoteChars());
    }

    @Test
    void testSetEscapeCharsWithNull() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        delimiter.setEscapeChars(null);

        // then
        assertNull(delimiter.getEscapeChars());
    }

    @Test
    void testSetQuoteCharsWithEmptyArray() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        delimiter.setQuoteChars(new char[] { });

        // then
        assertArrayEquals(new char[] { }, delimiter.getQuoteChars());
    }

    @Test
    void testSetEscapeCharsWithEmptyArray() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        delimiter.setEscapeChars(new char[] { });

        // then
        assertArrayEquals(new char[] { }, delimiter.getEscapeChars());
    }

    @Test
    void testIsEscaped() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        boolean resultSingle = delimiter.isEscaped("\\", 0);
        boolean resultDouble = delimiter.isEscaped("\\\\", 1);
        boolean resultTripleFirst = delimiter.isEscaped("\\\\\\", 1);
        boolean resultTripleSecond = delimiter.isEscaped("\\\\\\", 2);
        boolean resultEmpty = delimiter.isEscaped("", 0);
        boolean resultSingleChar = delimiter.isEscaped("a", 0);
        boolean resultNonEscapeChar = delimiter.isEscaped("ab", 1);

        // then
        assertFalse(resultSingle);
        assertTrue(resultDouble);
        assertTrue(resultTripleFirst);
        assertFalse(resultTripleSecond);
        assertFalse(resultEmpty);
        assertFalse(resultSingleChar);
        assertFalse(resultNonEscapeChar);
    }

    @Test
    void testIsEscapedWithNullEscapeChars() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();
        delimiter.setEscapeChars(null);

        // when
        boolean result1 = delimiter.isEscaped("a\\b", 1);
        boolean result2 = delimiter.isEscaped("a\\b", 2);
        boolean result3 = delimiter.isEscaped("\\\\", 1);

        // then
        assertFalse(result1);
        assertFalse(result2);
        assertFalse(result3);
    }

    @Test
    void testIsDelimiterWithEscapedCharacters() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter();

        // when
        boolean resultEscapedSpace = delimiter.isDelimiter("a\\ b", 1);
        boolean resultNonEscapedSpace = delimiter.isDelimiter("a b", 1);

        // then
        assertFalse(resultEscapedSpace);
        assertTrue(resultNonEscapedSpace);
    }

    @Test
    void testIsDelimiterWithQuotedCharacters() {
        // given
        RenewArgumentCompleter.WhitespaceArgumentDelimiter delimiter =
            new RenewArgumentCompleter.WhitespaceArgumentDelimiter()
            {
                @Override
                public boolean isQuoted(CharSequence buffer, int pos) {
                    return pos >= 5 && pos <= 10;
                }
            };

        String buffer = "text \"quoted text\" more";

        // when:
        boolean isDelimiterInsideQuoted = delimiter.isDelimiter(buffer, 7);

        // then
        assertFalse(isDelimiterInsideQuoted);
    }

    @Test
    void testCompleteInNonStrictModeWithInvalidPreviousArgument() {
        // given
        TestCompleter completer1 = new TestCompleter("valid");
        TestCompleter completer2 = new TestCompleter("completion");
        RenewArgumentCompleter rac = new RenewArgumentCompleter(completer1, completer2);
        rac.setStrict(false);

        LinkedList<CharSequence> candidates = new LinkedList<>();
        // when
        int result = rac.complete("invalid comp", 11, candidates);

        // then
        assertEquals(8, result);
        assertEquals(1, candidates.size());
        assertEquals("completion", candidates.getFirst());
    }

    private static class TestCompleter implements Completer {
        private final String completion;

        public TestCompleter() {
            this(null);
        }

        public TestCompleter(String completion) {
            this.completion = completion;
        }

        @Override
        public int complete(String buffer, int cursor, List<CharSequence> candidates) {
            if (completion != null && buffer != null && completion.startsWith(buffer)) {
                candidates.add(completion);
                return 0;
            }
            return -1;
        }
    }
}
