package de.renew.faformalism.util;

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

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 de.renew.unify.Tuple;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;

class FAAutomatonSimulationHelperTest {

    private static StackDataStructure initializeStack(String content) {
        StackDataStructure stack = new StackDataStructure();
        for (char c : content.toCharArray()) {
            stack.push(c);
        }
        return stack;
    }

    private static Tuple createTuple(String word, StackDataStructure stack) {
        return new Tuple(new Object[] { word, stack }, null);
    }

    public static Stream<Arguments> canFireNFAForSimulateWordProviderPositiveProvider() {
        return Stream.of(
            arguments(new Tuple(new Object[] { "a", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "a+b", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "a+b", new StackDataStructure() }, null), "b"),
            arguments(new Tuple(new Object[] { "a+b", new StackDataStructure() }, null), "a,b"),
            arguments(new Tuple(new Object[] { "a+b", new StackDataStructure() }, null), ""),
            arguments(new Tuple(new Object[] { "a^+", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "a*", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "a+b+c", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "a+b+c", new StackDataStructure() }, null), "c"),
            arguments(new Tuple(new Object[] { "(a+b)+c", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "(a+b)+c", new StackDataStructure() }, null), "b"),
            arguments(new Tuple(new Object[] { "(a+b)+c", new StackDataStructure() }, null), "c"),
            arguments(new Tuple(new Object[] { "a+(b+c)", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "a+(b+c)", new StackDataStructure() }, null), "b"),
            arguments(new Tuple(new Object[] { "a+(b+c)", new StackDataStructure() }, null), "c"),
            arguments(new Tuple(new Object[] { "(a+b+c)", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "(a+b+c)", new StackDataStructure() }, null), "b"),
            arguments(new Tuple(new Object[] { "(a+b+c)", new StackDataStructure() }, null), "c"),
            arguments(
                new Tuple(new Object[] { "(((a+b)))+c", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "a°", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "(a+b)°", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "a°+b°", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "(a+b)°", new StackDataStructure() }, null), "b"),
            arguments(new Tuple(new Object[] { "a°+b°", new StackDataStructure() }, null), "b"),
            arguments(new Tuple(new Object[] { "a°+b°", new StackDataStructure() }, null), "a,b"),
            arguments(new Tuple(new Object[] { "a", new StackDataStructure() }, null), "a,"),
            arguments(new Tuple(new Object[] { "a", new StackDataStructure() }, null), ",a"),
            arguments(new Tuple(new Object[] { "a", new StackDataStructure() }, null), "b,"),
            arguments(
                new Tuple(new Object[] { "((a*b+c)*+(bd)^+)*", new StackDataStructure() }, null),
                "a"));
    }

    @ParameterizedTest
    @MethodSource("canFireNFAForSimulateWordProviderPositiveProvider")
    void testCanFireNFAForSimulateWordPositive(Tuple currentTuple, String arcInscription) {
        //Given
        SimulationSettingsManager.setAutomatonModel(FAAutomatonModelEnum.NFA);
        //When
        SimulationSettingsManager.setSimulateWordMode(true);
        //Then
        assertTrue(FAAutomatonSimulationHelper.canFire(currentTuple, arcInscription));
    }

    public static Stream<Arguments> canFireNFAForSimulateWordNegativeProvider() {
        return Stream.of(
            arguments(new Tuple(new Object[] { "a", new StackDataStructure() }, null), "b"),
            arguments(new Tuple(new Object[] { "a+a", new StackDataStructure() }, null), "b"),
            arguments(new Tuple(new Object[] { "a+b", new StackDataStructure() }, null), "c"),
            arguments(new Tuple(new Object[] { "a+b", new StackDataStructure() }, null), "c,d"),
            arguments(new Tuple(new Object[] { "", new StackDataStructure() }, null), "a"),
            arguments(new Tuple(new Object[] { "a^+", new StackDataStructure() }, null), "b"),
            arguments(new Tuple(new Object[] { "a*", new StackDataStructure() }, null), "b"),
            arguments(new Tuple(new Object[] { "a+b+c", new StackDataStructure() }, null), "d"),
            arguments(new Tuple(new Object[] { "(a+b)°", new StackDataStructure() }, null), "c"));
    }

    @ParameterizedTest
    @MethodSource("canFireNFAForSimulateWordNegativeProvider")
    void testCanFireNFAForSimulateWordNegative(Tuple currentTuple, String arcInscription) {
        //Given
        SimulationSettingsManager.setAutomatonModel(FAAutomatonModelEnum.NFA);
        //When
        SimulationSettingsManager.setSimulateWordMode(true);
        //Then
        assertFalse(FAAutomatonSimulationHelper.canFire(currentTuple, arcInscription));
    }

    public static Stream<Arguments> canFirePDAForSimulateWordModePositiveProvider() {
        return Stream.of(
            arguments(new Tuple(new Object[] { "a", new StackDataStructure() }, null), "a,  ->"),
            arguments(new Tuple(new Object[] { "a+b", initializeStack("ab") }, null), "a,b->"),
            arguments(new Tuple(new Object[] { "a+b", initializeStack("ab") }, null), ",->"),
            arguments(
                new Tuple(new Object[] { "a+(b+c)", initializeStack("abd") }, null), "b,d->s"),
            arguments(new Tuple(new Object[] { "(a+b)°", initializeStack("") }, null), "b,->s"),
            arguments(
                new Tuple(new Object[] { "(a+b)°", initializeStack("a") }, null), "b,   a  ->s"),
            arguments(
                new Tuple(new Object[] { "((a*b+c)*+(bd)^+)*", initializeStack("0") }, null),
                "b,0->s"));
    }

    @ParameterizedTest
    @MethodSource("canFirePDAForSimulateWordModePositiveProvider")
    void testCanFirePDAForSimulateWordModePositive(Tuple currentTuple, String arcInscription) {
        //given
        SimulationSettingsManager.setSimulateWordMode(true);
        SimulationSettingsManager.setAutomatonModel(FAAutomatonModelEnum.PDA);
        //when / then
        assertTrue(FAAutomatonSimulationHelper.canFire(currentTuple, arcInscription));
    }

    public static Stream<Arguments> canFirePDAForSimulateWordModeNegative() {
        return Stream.of(
            arguments(new Tuple(new Object[] { "a", new StackDataStructure() }, null), "b,->"),
            arguments(new Tuple(new Object[] { "a+b", initializeStack("ab") }, null), "a,e->"),
            arguments(new Tuple(new Object[] { "a+(b+c)", initializeStack("abd") }, null), "d,->"),
            arguments(new Tuple(new Object[] { "(a+b)°", initializeStack("") }, null), "b,a->s"),
            arguments(
                new Tuple(new Object[] { "((a*b+c)*+(bd)^+)*", initializeStack("01") }, null),
                "b,0->s"));
    }

    @ParameterizedTest
    @MethodSource("canFirePDAForSimulateWordModeNegative")
    void testCanFirePDAForSimulateWordModeNegative(Tuple currentTuple, String arcInscription) {
        //given
        SimulationSettingsManager.setSimulateWordMode(true);
        SimulationSettingsManager.setAutomatonModel(FAAutomatonModelEnum.PDA);
        //when / then
        assertFalse(FAAutomatonSimulationHelper.canFire(currentTuple, arcInscription));
    }

    public static Stream<Arguments> canFireNFAForBuildModeWordProvider() {
        return Stream.of(
            arguments(new Tuple(new Object[] { "", null }, null), ""),
            arguments(new Tuple(new Object[] { "a", null }, null), "a"),
            arguments(new Tuple(new Object[] { "(a+b)*", null }, null), "a"),
            arguments(new Tuple(new Object[] { "(a+b)*", null }, null), "a"),
            arguments(new Tuple(new Object[] { "(a+b)*", null }, null), "x"),
            arguments(new Tuple(new Object[] { "a+b", null }, null), ""), arguments(
                new Tuple(new Object[] { "(a+b)*(aa)^+((a+b)*(aa)^++aa)°", null }, null), "a,b"));
    }

    @ParameterizedTest
    @MethodSource("canFireNFAForBuildModeWordProvider")
    void testCanFireNFAForBuildWordMode(Tuple currentTupel, String arcInscription) {
        //Given
        SimulationSettingsManager.setAutomatonModel(FAAutomatonModelEnum.NFA);
        //When/then
        SimulationSettingsManager.setSimulateWordMode(false);
        assertTrue(FAAutomatonSimulationHelper.canFire(currentTupel, arcInscription));
    }

    static Stream<Arguments> fireNFASimulateWordModeProvider() {
        return Stream.of(
            arguments(createTuple("a", null), createTuple("", null), "a"),
            arguments(createTuple("ab", null), createTuple("b", null), "a"),
            arguments(createTuple("a*", null), createTuple("a*", null), "a"),
            arguments(createTuple("a^*", null), createTuple("a^*", null), "a"),
            arguments(createTuple("a+b", null), createTuple("", null), "a"),
            arguments(createTuple("a*+b", null), createTuple("a*", null), "a"),
            arguments(createTuple("a^*+b", null), createTuple("a^*", null), "a"),
            arguments(createTuple("a^++b", null), createTuple("a^*", null), "a"),
            arguments(createTuple("(a^++b)", null), createTuple("a^*", null), "a"),
            arguments(createTuple("a^+b+b", null), createTuple("a^*b", null), "a"),
            arguments(createTuple("()a^+b+b", null), createTuple("a^*b", null), "a"),
            arguments(createTuple("a", null), createTuple("a", null), ""),
            arguments(createTuple("a", null), createTuple("a", null), ",b"),
            arguments(createTuple("c", null), createTuple("c", null), "a,b,"));
    }

    @ParameterizedTest
    @MethodSource("fireNFASimulateWordModeProvider")
    void testFireNFASimulateWordMode(
        Tuple currentTuple, Tuple expectedTuple, String arcInscription)
    {
        //given
        SimulationSettingsManager.setSimulateWordMode(true);
        SimulationSettingsManager.setAutomatonModel(FAAutomatonModelEnum.NFA);
        SimulationSettingsManager.setManualSimulation(false);
        //when
        Tuple actualTuple = FAAutomatonSimulationHelper.fire(currentTuple, arcInscription);
        //then
        assertEquals(expectedTuple.getComponent(0), actualTuple.getComponent(0));
    }

    static Stream<Arguments> firePDASimulateWordModeProvider() {
        return Stream.of(
            arguments(
                createTuple("", initializeStack("")), createTuple("", initializeStack("")), ",->"),
            arguments(
                createTuple("a", initializeStack("b")), createTuple("ε", initializeStack("bb")),
                "a,->b"),
            arguments(
                createTuple("", initializeStack("")), createTuple("", initializeStack("b")),
                ",->b"),
            arguments(
                createTuple("1122", initializeStack("1")),
                createTuple("122", initializeStack("11")), "1,->1"),
            arguments(
                createTuple("a", initializeStack("b")), createTuple("a", initializeStack("")),
                ",b->"),
            arguments(
                createTuple("ab", initializeStack("b")), createTuple("b", initializeStack("bb")),
                "a,->b\na,c->b"),
            arguments(
                createTuple("x", initializeStack("y")), createTuple("x", initializeStack("z")),
                ",y->z"),
            arguments(
                createTuple("b", initializeStack("X")), createTuple("ε", initializeStack("")),
                "b,X->"));
    }

    @ParameterizedTest
    @MethodSource("firePDASimulateWordModeProvider")
    void testFirePDASimulateWordMode(
        Tuple currentTuple, Tuple expectedTuple, String arcInscription)
    {
        //given

        SimulationSettingsManager.setAutomatonModel(FAAutomatonModelEnum.PDA);
        //when
        Tuple actualTuple = FAAutomatonSimulationHelper.fire(currentTuple, arcInscription);
        //then
        assertAll(
            () -> assertEquals(expectedTuple.getComponent(0), actualTuple.getComponent(0)),
            () -> assertEquals(expectedTuple.getComponent(1), actualTuple.getComponent(1)));
    }

    static Stream<Arguments> firePDABuildWordModeProvider() {
        return Stream.of(
            arguments(
                createTuple("a", initializeStack("b")), createTuple("aa", initializeStack("bb")),
                "a,->b"),
            arguments(
                createTuple("aabb", initializeStack("$")),
                createTuple("aabba", initializeStack("$")), "a,$->$"),
            arguments(
                createTuple("", initializeStack("")), createTuple("ε", initializeStack("")), ",->"),
            arguments(
                createTuple("a", initializeStack("")), createTuple("a", initializeStack("")),
                ",->"),
            arguments(
                createTuple("1122", initializeStack("1")),
                createTuple("11221", initializeStack("11")), "1,->1"));
    }

    @ParameterizedTest
    @MethodSource("firePDABuildWordModeProvider")
    void testFirePDABuildWordMode(Tuple currentTuple, Tuple expectedTuple, String arcInscription) {
        //given
        SimulationSettingsManager.setSimulateWordMode(false);
        SimulationSettingsManager.setAutomatonModel(FAAutomatonModelEnum.PDA);
        //when
        Tuple actualTuple = FAAutomatonSimulationHelper.fire(currentTuple, arcInscription);
        //then
        assertAll(
            () -> assertEquals(expectedTuple.getComponent(0), actualTuple.getComponent(0)),
            () -> assertEquals(expectedTuple.getComponent(1), actualTuple.getComponent(1)));
    }

    static Stream<Arguments> tokenConsumptionInputAndListProvider() {
        return Stream.of(
            arguments("", new HashSet<>(List.of(""))), arguments("a", new HashSet<>(List.of("a"))),
            arguments("a*", new HashSet<>(Arrays.asList("", "aa*"))),
            arguments("a^*", new HashSet<>(Arrays.asList("", "aa^*"))),
            arguments("a^+", new HashSet<>(List.of("aa^*"))),
            arguments("a°", new HashSet<>(Arrays.asList("aa°"))),
            //"a°", but the encoding seems to be wrong
            arguments("a^°", new HashSet<>(List.of("aa^°"))),
            arguments("a+b", new HashSet<>(Arrays.asList("a", "b"))),
            arguments("a|b", new HashSet<>(Arrays.asList("a", "b"))),
            arguments("a+b+", new HashSet<>(Arrays.asList("", "a", "b"))),
            arguments("a|b|", new HashSet<>(Arrays.asList("", "a", "b"))),
            arguments("a*+b^++", new HashSet<>(Arrays.asList("", "aa*", "bb^*"))),
            arguments("()", new HashSet<>(List.of(""))),
            arguments("()a", new HashSet<>(List.of("a"))),
            arguments("(())", new HashSet<>(List.of(""))),
            arguments("(())a", new HashSet<>(List.of("a"))),
            arguments("()(a+b)", new HashSet<>(Arrays.asList("a", "b"))),
            arguments("()*(a+b)", new HashSet<>(Arrays.asList("a", "b"))),
            arguments("()°(a+b)", new HashSet<>(Arrays.asList("a", "b"))),
            arguments("(a*+b*)", new HashSet<>(Arrays.asList("", "aa*", "bb*"))),
            arguments("(a*)^+", new HashSet<>(Arrays.asList("", "(a*)^*", "aa*(a*)^*"))),
            arguments("(a+b)^+", new HashSet<>(Arrays.asList("a(a+b)^*", "b(a+b)^*"))),
            arguments("(a+b)^*", new HashSet<>(Arrays.asList("", "a(a+b)^*", "b(a+b)^*"))),
            arguments(
                "(a+b)^*(c+d)^*",
                new HashSet<>(
                    Arrays
                        .asList("", "a(a+b)^*(c+d)^*", "b(a+b)^*(c+d)^*", "c(c+d)^*", "d(c+d)^*"))),
            arguments(
                "(a+b)^*(c+d)^*(e+f)^+",
                new HashSet<>(
                    Arrays.asList(
                        "a(a+b)^*(c+d)^*(e+f)^+", "b(a+b)^*(c+d)^*(e+f)^+", "c(c+d)^*(e+f)^+",
                        "d(c+d)^*(e+f)^+", "e(e+f)^*", "f(e+f)^*"))),
            arguments(
                "((a+b)*+c)^*d^++e*",
                new HashSet<>(
                    Arrays.asList(
                        "", "ee*", "((a+b)*+c)^*d^+", "a(a+b)*((a+b)*+c)^*d^+",
                        "b(a+b)*((a+b)*+c)^*d^+", "c((a+b)*+c)^*d^+", "dd^*"))),
            arguments(
                "(a+b)*(aa)^+((a+b)*(aa)^++aa)°",
                new HashSet<>(
                    Arrays.asList(
                        "a(a+b)*(aa)^+((a+b)*(aa)^++aa)°", "b(a+b)*(aa)^+((a+b)*(aa)^++aa)°",
                        "aa(aa)^*((a+b)*(aa)^++aa)°"))),
            arguments(
                "((a+b)*(aa)^++aa)°",
                new HashSet<>(
                    Arrays.asList(
                        "aa((a+b)*(aa)^++aa)°", "aa(aa)^*((a+b)*(aa)^++aa)°",
                        "a(a+b)*(aa)^+((a+b)*(aa)^++aa)°", "b(a+b)*(aa)^+((a+b)*(aa)^++aa)°"))),
            arguments(
                "b*a(a+b)(b+ab*a(a+b))°",
                new HashSet<>(Arrays.asList("bb*a(a+b)(b+ab*a(a+b))°", "a(a+b)(b+ab*a(a+b))°"))),
            arguments(
                "(a+b)(b+ab*a(a+b))°",
                new HashSet<>(Arrays.asList("a(b+ab*a(a+b))°", "b(b+ab*a(a+b))°"))),
            arguments(
                "(b+ab*a(a+b))°",
                new HashSet<>(Arrays.asList("b(b+ab*a(a+b))°", "ab*a(a+b)(b+ab*a(a+b))°"))),
            arguments(
                "b*a(a+b)(b+ab*a(a+b))°",
                new HashSet<>(Arrays.asList("bb*a(a+b)(b+ab*a(a+b))°", "a(a+b)(b+ab*a(a+b))°"))));
    }

    @ParameterizedTest
    @MethodSource("tokenConsumptionInputAndListProvider")
    void testPrepareTokenForConsumption(String input, HashSet<String> expectedResult) {
        assertEquals(
            expectedResult,
            new HashSet<>(FAAutomatonSimulationHelper.prepareTokenForConsumption(input)));
    }

    @Test
    void nullTestPrepareTokenForConsumption() {
        assertThrows(
            IllegalStateException.class,
            () -> FAAutomatonSimulationHelper.prepareTokenForConsumption(null));
    }

    public static Stream<Arguments> splitNFAArcInscriptionInputAndListProvider() {
        return Stream.of(
            arguments("", new HashSet<>(List.of(""))), arguments("a", new HashSet<>(List.of("a"))),
            arguments("a,b", new HashSet<>(Arrays.asList("a", "b"))),
            arguments("a, b", new HashSet<>(Arrays.asList("a", "b"))),
            arguments("a,", new HashSet<>(Arrays.asList("a", ""))));
    }

    @ParameterizedTest
    @MethodSource("splitNFAArcInscriptionInputAndListProvider")
    void testSplitNFAArcInscription(String input, HashSet<String> expectedResult) {
        assertThrows(
            IllegalStateException.class,
            () -> FAAutomatonSimulationHelper.splitNFAArcInscription(null));
        assertEquals(
            expectedResult,
            new HashSet<>(FAAutomatonSimulationHelper.splitNFAArcInscription(input)));
    }
}
