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 RichEventIsCorrectPredicateTest {

    @Test
    void shouldFilterEnumConstantsByRichEventIsCorrectPredicate(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanProject(tempDir);

        List<String> all = List.of(
                "com.example.order.OrderCommand.PAY",
                "com.example.order.OrderCommand.SHIP",
                "com.example.order.OrderCommand.META");
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.service.AbstractOrderService")
                .methodName("dispatch")
                .build();

        List<String> filtered = RichEventPredicateEvaluator.filterEnumConstants(
                all, "myEvent.isCorrect()", "com.example.order.OrderCommand", trigger, context);

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

    @Test
    void shouldInferPolymorphicEventsForRichEventConstraint(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanProject(tempDir);

        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.service.AbstractOrderService")
                .methodName("dispatch")
                .event("myEvent.getType()")
                .constraint("myEvent.isCorrect()")
                .external(true)
                .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.isAmbiguous()).isTrue();
    }

    @Test
    void shouldLinkTransitionsWhenConstraintUsesRichEventPredicate(@TempDir Path tempDir) throws Exception {
        CodebaseContext context = scanProject(tempDir);

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .className("com.example.service.AbstractOrderService")
                        .methodName("dispatch")
                        .event("myEvent.getType()")
                        .polymorphicEvents(List.of(
                                "com.example.order.OrderCommand.PAY",
                                "com.example.order.OrderCommand.SHIP",
                                "com.example.order.OrderCommand.META"))
                        .constraint("myEvent.isCorrect()")
                        .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(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
        assertThat(result.getMetadata().getCallChains().get(0).getLinkResolution())
                .isEqualTo(click.kamil.springstatemachineexporter.analysis.model.LinkResolution.AMBIGUOUS_WIDEN);
    }

    private static CodebaseContext scanProject(Path tempDir) throws Exception {
        Path javaRoot = tempDir.resolve("src/main/java/com/example");
        Files.createDirectories(javaRoot.resolve("order"));
        Files.createDirectories(javaRoot.resolve("domain"));
        Files.createDirectories(javaRoot.resolve("service"));
        Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }");
        Files.writeString(javaRoot.resolve("order/OrderCommand.java"), """
                package com.example.order;
                public enum OrderCommand { PAY, SHIP, META }
                """);
        Files.writeString(javaRoot.resolve("domain/DomainEvent.java"), """
                package com.example.domain;
                import com.example.order.OrderCommand;
                public interface DomainEvent {
                    OrderCommand getType();
                    boolean isCorrect();
                }
                """);
        Files.writeString(javaRoot.resolve("domain/PayEvent.java"), """
                package com.example.domain;
                import com.example.order.OrderCommand;
                public class PayEvent implements DomainEvent {
                    @Override
                    public OrderCommand getType() { return OrderCommand.PAY; }
                    @Override
                    public boolean isCorrect() { return true; }
                }
                """);
        Files.writeString(javaRoot.resolve("domain/ShipEvent.java"), """
                package com.example.domain;
                import com.example.order.OrderCommand;
                public class ShipEvent implements DomainEvent {
                    @Override
                    public OrderCommand getType() { return OrderCommand.SHIP; }
                    @Override
                    public boolean isCorrect() { return true; }
                }
                """);
        Files.writeString(javaRoot.resolve("domain/MetaEvent.java"), """
                package com.example.domain;
                import com.example.order.OrderCommand;
                public class MetaEvent implements DomainEvent {
                    @Override
                    public OrderCommand getType() { return OrderCommand.META; }
                    @Override
                    public boolean isCorrect() { return false; }
                }
                """);
        Files.writeString(javaRoot.resolve("service/AbstractOrderService.java"), """
                package com.example.service;
                import com.example.domain.DomainEvent;
                import com.example.order.OrderCommand;
                public abstract class AbstractOrderService {
                    protected void dispatch(DomainEvent myEvent) {
                        if (myEvent.isCorrect()) {
                            fire(myEvent.getType());
                        }
                    }
                    protected abstract void fire(OrderCommand event);
                }
                """);
        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;
    }
}
