package click.kamil.springstatemachineexporter.analysis.resolver;

import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver.MachineTypes;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.Event;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class EnumMemberPredicatePolymorphicInferenceTest {

    @Test
    void shouldExtractAnyBooleanMemberPredicateFromConstraint() {
        assertThat(EnumMemberPredicateEvaluator.extractPredicateCalls("eventType.isEvent()"))
                .containsExactly(new EnumMemberPredicateEvaluator.PredicateCall("eventType", "isEvent", false));
        assertThat(EnumMemberPredicateEvaluator.extractPredicateCalls("!cmd.isActive()"))
                .containsExactly(new EnumMemberPredicateEvaluator.PredicateCall("cmd", "isActive", true));
    }

    @Test
    void shouldFilterEnumConstantsByGenericBooleanMember(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanEnumProject(tempDir, """
                package com.example.order;
                public enum OrderCommand {
                    PAY(true),
                    META(false);
                    private final boolean event;
                    OrderCommand(boolean event) { this.event = event; }
                    public boolean isEvent() { return event; }
                }
                """);

        List<String> all = List.of(
                "com.example.order.OrderCommand.PAY",
                "com.example.order.OrderCommand.META");
        List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
                all, "eventType.isEvent()", "com.example.order.OrderCommand", context);

        assertThat(filtered).containsExactly("com.example.order.OrderCommand.PAY");
    }

    @Test
    void shouldFilterEnumConstantsBySwitchBasedPredicate(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanEnumProject(tempDir, """
                package com.example.order;
                public enum OrderCommand {
                    PAY,
                    SHIP,
                    META;
                    public boolean isEvent() {
                        return switch (this) {
                            case PAY, SHIP -> true;
                            default -> false;
                        };
                    }
                }
                """);

        List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
                List.of(
                        "com.example.order.OrderCommand.PAY",
                        "com.example.order.OrderCommand.SHIP",
                        "com.example.order.OrderCommand.META"),
                "eventType.isEvent()",
                "com.example.order.OrderCommand",
                context);

        assertThat(filtered).containsExactly(
                "com.example.order.OrderCommand.PAY",
                "com.example.order.OrderCommand.SHIP");
    }

    @Test
    void shouldPreferTransitionEventsOverFullEnumOnDynamicTrigger(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanEnumProject(tempDir, """
                package com.example.order;
                public enum OrderCommand {
                    PAY(true),
                    SHIP(true),
                    META(false);
                    private final boolean event;
                    OrderCommand(boolean event) { this.event = event; }
                    public boolean isEvent() { return event; }
                }
                """);

        TriggerPoint trigger = TriggerPoint.builder()
                .event("OrderCommand.valueOf(name)")
                .constraint("eventType.isEvent()")
                .build();
        MachineTypes types = new MachineTypes(null, "com.example.order.OrderCommand");
        List<Transition> transitions = List.of(
                transition("OrderCommand.PAY", "NEW", "PAID"),
                transition("OrderCommand.SHIP", "PAID", "SHIPPED"));

        TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
                trigger, types, context, transitions, true);

        assertThat(enriched.getPolymorphicEvents())
                .containsExactly(
                        "com.example.order.OrderCommand.PAY",
                        "com.example.order.OrderCommand.SHIP");
        assertThat(enriched.isAmbiguous()).isTrue();
    }

    @Test
    void shouldNotWidenSingleConcretePolymorphicEventWhenConstraintIsNull() {
        TriggerPoint trigger = TriggerPoint.builder()
                .event("event")
                .polymorphicEvents(List.of("com.example.order.OrderCommand.PAY"))
                .constraint(null)
                .ambiguous(false)
                .build();
        MachineTypes types = new MachineTypes(null, "com.example.order.OrderCommand");
        List<Transition> transitions = List.of(
                transition("OrderCommand.PAY", "NEW", "PAID"),
                transition("OrderCommand.SHIP", "PAID", "SHIPPED"));

        TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
                trigger, types, null, transitions, true);

        assertThat(enriched.getPolymorphicEvents()).containsExactly("com.example.order.OrderCommand.PAY");
        assertThat(enriched.isAmbiguous()).isFalse();
    }

    @Test
    void shouldFilterUsingNonIsPrefixedPredicateMethod(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanEnumProject(tempDir, """
                package com.example.order;
                public enum OrderCommand {
                    PAY(true),
                    AUDIT(false);
                    private final boolean fires;
                    OrderCommand(boolean fires) { this.fires = fires; }
                    public boolean canFire() { return fires; }
                }
                """);

        List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
                List.of(
                        "com.example.order.OrderCommand.PAY",
                        "com.example.order.OrderCommand.AUDIT"),
                "eventType.canFire()",
                "com.example.order.OrderCommand",
                context);

        assertThat(filtered).containsExactly("com.example.order.OrderCommand.PAY");
    }

    @Test
    void shouldFilterEnterpriseInterfaceOverrideStyle(@TempDir Path tempDir) throws Exception {
        Path javaRoot = tempDir.resolve("src/main/java/com/example");
        Files.createDirectories(javaRoot.resolve("core"));
        Files.createDirectories(javaRoot.resolve("order"));
        Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }");
        Files.writeString(javaRoot.resolve("core/TriggerClassifier.java"), """
                package com.example.core;
                public interface TriggerClassifier {
                    boolean isEvent();
                }
                """);
        Files.writeString(javaRoot.resolve("order/OrderEvent.java"), """
                package com.example.order;
                import com.example.core.TriggerClassifier;
                public enum OrderEvent implements TriggerClassifier {
                    PAY(true),
                    LOG(false);
                    private final boolean event;
                    OrderEvent(boolean event) { this.event = event; }
                    @Override
                    public boolean isEvent() { return this.event; }
                }
                """);
        CodebaseContext context = new CodebaseContext();
        context.setSourcepath(List.of(tempDir.resolve("src/main/java").toString()));
        context.scan(tempDir);

        List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
                List.of("com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent.LOG"),
                "eventType.isEvent()",
                "com.example.order.OrderEvent",
                context);

        assertThat(filtered).containsExactly("com.example.order.OrderEvent.PAY");
    }

    @Test
    void shouldNarrowOverBroadPolymorphicEventsToTransitionSubset(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanEnumProject(tempDir, """
                package com.example.order;
                public enum OrderCommand {
                    PAY(true),
                    SHIP(true),
                    META(false);
                    private final boolean event;
                    OrderCommand(boolean event) { this.event = event; }
                    public boolean isEvent() { return event; }
                }
                """);

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("event")
                        .polymorphicEvents(List.of(
                                "com.example.order.OrderCommand.PAY",
                                "com.example.order.OrderCommand.SHIP",
                                "com.example.order.OrderCommand.META"))
                        .constraint("eventType.isEvent()")
                        .ambiguous(true)
                        .external(true)
                        .build())
                .build();

        Transition pay = transition("OrderCommand.PAY", "NEW", "PAID");
        Transition ship = transition("OrderCommand.SHIP", "PAID", "SHIPPED");
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .eventTypeFqn("com.example.order.OrderCommand")
                .transitions(List.of(pay, ship))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        new TransitionLinkerEnricher().enrich(result, context, null);

        TriggerPoint tp = result.getMetadata().getCallChains().get(0).getTriggerPoint();
        assertThat(tp.getPolymorphicEvents())
                .containsExactlyInAnyOrder(
                        "com.example.order.OrderCommand.PAY",
                        "com.example.order.OrderCommand.SHIP");
        assertThat(tp.getPolymorphicEvents())
                .doesNotContain("com.example.order.OrderCommand.META");
        assertThat(tp.isAmbiguous()).isTrue();
        // Multiple predicate-true constants without per-endpoint dataflow must not highlight every transition.
        assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
        assertThat(result.getMetadata().getCallChains().get(0).getLinkResolution())
                .isEqualTo(click.kamil.springstatemachineexporter.analysis.model.LinkResolution.AMBIGUOUS_WIDEN);
    }

    @Test
    void shouldNarrowEqualSizePolyListWithNonTransitionConstantsWhenConstraintIsNull(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanEnumProject(tempDir, """
                package com.example.order;
                public enum OrderCommand {
                    PAY,
                    SHIP,
                    META;
                }
                """);

        TriggerPoint trigger = TriggerPoint.builder()
                .event("event")
                .polymorphicEvents(List.of(
                        "com.example.order.OrderCommand.PAY",
                        "com.example.order.OrderCommand.SHIP",
                        "com.example.order.OrderCommand.META"))
                .constraint(null)
                .ambiguous(true)
                .build();
        MachineTypes types = new MachineTypes(null, "com.example.order.OrderCommand");
        List<Transition> transitions = List.of(
                transition("OrderCommand.PAY", "NEW", "PAID"),
                transition("OrderCommand.SHIP", "PAID", "SHIPPED"));

        TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
                trigger, types, context, transitions, true);

        assertThat(enriched.getPolymorphicEvents())
                .containsExactlyInAnyOrder(
                        "com.example.order.OrderCommand.PAY",
                        "com.example.order.OrderCommand.SHIP");
        assertThat(enriched.getPolymorphicEvents())
                .doesNotContain("com.example.order.OrderCommand.META");
    }

    @Test
    void shouldFailClosedWhenPredicateMethodCannotBeResolved(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanEnumProject(tempDir, """
                package com.example.order;
                public enum OrderCommand {
                    PAY,
                    SHIP,
                    META;
                }
                """);

        TriggerPoint trigger = TriggerPoint.builder()
                .event("OrderCommand.valueOf(name)")
                .constraint("eventType.isEvent()")
                .build();
        MachineTypes types = new MachineTypes(null, "com.example.order.OrderCommand");
        List<Transition> transitions = List.of(
                transition("OrderCommand.PAY", "NEW", "PAID"),
                transition("OrderCommand.SHIP", "PAID", "SHIPPED"));

        TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
                trigger, types, context, transitions, true);

        assertThat(enriched.getPolymorphicEvents()).isNullOrEmpty();
    }

    @Test
    void shouldExcludeConstantsWhenPredicateMethodCannotBeEvaluated(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanEnumProject(tempDir, """
                package com.example.order;
                public enum OrderCommand {
                    PAY,
                    META;
                }
                """);

        List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
                List.of(
                        "com.example.order.OrderCommand.PAY",
                        "com.example.order.OrderCommand.META"),
                "eventType.isEvent()",
                "com.example.order.OrderCommand",
                context);

        assertThat(filtered).isEmpty();
    }

    @Test
    void shouldEvaluateInterfaceDefaultPredicateDelegation(@TempDir Path tempDir) throws Exception {
        Path javaRoot = tempDir.resolve("src/main/java/com/example");
        Files.createDirectories(javaRoot.resolve("core"));
        Files.createDirectories(javaRoot.resolve("order"));
        Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }");
        Files.writeString(javaRoot.resolve("core/TriggerClassifier.java"), """
                package com.example.core;
                public interface TriggerClassifier {
                    default boolean isEvent() { return isTrigger(); }
                    boolean isTrigger();
                }
                """);
        Files.writeString(javaRoot.resolve("order/OrderEvent.java"), """
                package com.example.order;
                import com.example.core.TriggerClassifier;
                public enum OrderEvent implements TriggerClassifier {
                    PAY(true),
                    LOG(false);
                    private final boolean trigger;
                    OrderEvent(boolean trigger) { this.trigger = trigger; }
                    @Override
                    public boolean isTrigger() { return this.trigger; }
                }
                """);
        CodebaseContext context = new CodebaseContext();
        context.setSourcepath(List.of(tempDir.resolve("src/main/java").toString()));
        context.scan(tempDir);

        List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
                List.of("com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent.LOG"),
                "eventType.isEvent()",
                "com.example.order.OrderEvent",
                context);

        assertThat(filtered).containsExactly("com.example.order.OrderEvent.PAY");
    }

    @Test
    void shouldInferPolymorphicEventsForNullPolyExternalTriggerWithConstraint(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanEnumProject(tempDir, """
                package com.example.order;
                public enum OrderCommand {
                    PAY(true),
                    SHIP(true),
                    META(false);
                    private final boolean event;
                    OrderCommand(boolean event) { this.event = event; }
                    public boolean isEvent() { return event; }
                }
                """);

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("eventType")
                        .constraint("eventType.isEvent()")
                        .external(true)
                        .ambiguous(true)
                        .build())
                .build();

        Transition pay = transition("OrderCommand.PAY", "NEW", "PAID");
        Transition ship = transition("OrderCommand.SHIP", "PAID", "SHIPPED");
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .eventTypeFqn("com.example.order.OrderCommand")
                .transitions(List.of(pay, ship))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        new TransitionLinkerEnricher().enrich(result, context, null);

        TriggerPoint tp = result.getMetadata().getCallChains().get(0).getTriggerPoint();
        assertThat(tp.getPolymorphicEvents())
                .containsExactlyInAnyOrder(
                        "com.example.order.OrderCommand.PAY",
                        "com.example.order.OrderCommand.SHIP");
        assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
        assertThat(result.getMetadata().getCallChains().get(0).getLinkResolution())
                .isEqualTo(click.kamil.springstatemachineexporter.analysis.model.LinkResolution.AMBIGUOUS_WIDEN);
    }

    @Test
    void shouldEvaluateDirectFieldReturnInPredicateMethod(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanEnumProject(tempDir, """
                package com.example.order;
                public enum OrderCommand {
                    PAY(true),
                    META(false);
                    private final boolean event;
                    OrderCommand(boolean event) { this.event = event; }
                    public boolean isEvent() { return event; }
                }
                """);

        List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
                List.of(
                        "com.example.order.OrderCommand.PAY",
                        "com.example.order.OrderCommand.META"),
                "eventType.isEvent()",
                "com.example.order.OrderCommand",
                context);

        assertThat(filtered).containsExactly("com.example.order.OrderCommand.PAY");
    }

    @Test
    void shouldNotMatchNonEventConstantsWhenPolyListWasOverBroad(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanEnumProject(tempDir, """
                package com.example.order;
                public enum OrderCommand {
                    PAY(true),
                    SHIP(true),
                    META(false);
                    private final boolean event;
                    OrderCommand(boolean event) { this.event = event; }
                    public boolean isEvent() { return event; }
                }
                """);

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("event")
                        .polymorphicEvents(List.of(
                                "com.example.order.OrderCommand.PAY",
                                "com.example.order.OrderCommand.SHIP",
                                "com.example.order.OrderCommand.META"))
                        .constraint("eventType.isEvent()")
                        .ambiguous(true)
                        .external(true)
                        .build())
                .build();

        Transition pay = transition("OrderCommand.PAY", "NEW", "PAID");
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .eventTypeFqn("com.example.order.OrderCommand")
                .transitions(List.of(pay))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        new TransitionLinkerEnricher().enrich(result, context, null);

        TriggerPoint tp = result.getMetadata().getCallChains().get(0).getTriggerPoint();
        assertThat(tp.getPolymorphicEvents()).containsExactly("com.example.order.OrderCommand.PAY");
        assertThat(tp.isAmbiguous()).isFalse();
        assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
    }

    private static CodebaseContext scanEnumProject(Path tempDir, String enumSource) throws Exception {
        Path javaRoot = tempDir.resolve("src/main/java/com/example/order");
        Files.createDirectories(javaRoot);
        Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }");
        Files.writeString(javaRoot.resolve("OrderCommand.java"), enumSource);
        CodebaseContext context = new CodebaseContext();
        context.setSourcepath(List.of(tempDir.resolve("src/main/java").toString()));
        context.scan(tempDir);
        return context;
    }

    private static Transition transition(String event, String source, String target) {
        Transition transition = new Transition();
        transition.setEvent(Event.of(event, event));
        transition.setSourceStates(List.of(State.of(source, source)));
        transition.setTargetStates(List.of(State.of(target, target)));
        return transition;
    }
}
