package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
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 TriggerMachineTypeRebinderTest {

    @Test
    void rebindsUnboundTypeVariablesFromSubclassParameterizationOnChain(@TempDir Path tempDir) throws Exception {
        writeSharedBaseFixture(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        TriggerPoint unbound = TriggerPoint.builder()
                .className("com.example.SharedService")
                .methodName("updateState")
                .event("event")
                .stateTypeFqn("S")
                .eventTypeFqn("E")
                .build();

        TriggerPoint rebound = TriggerMachineTypeRebinder.rebind(
                unbound,
                List.of(
                        "com.example.OrderController.pay()",
                        "com.example.OrderService.pay()",
                        "com.example.SharedService.updateState()"),
                context);

        assertThat(rebound.getStateTypeFqn()).isEqualTo("com.example.OrderState");
        assertThat(rebound.getEventTypeFqn()).isEqualTo("com.example.OrderEvent");
    }

    @Test
    void doesNotInventTypeWhenChainHasConflictingSubclassParameterizations(@TempDir Path tempDir) throws Exception {
        writeSharedBaseFixture(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        TriggerPoint unbound = TriggerPoint.builder()
                .className("com.example.SharedService")
                .methodName("updateState")
                .event("event")
                .stateTypeFqn("S")
                .eventTypeFqn("E")
                .build();

        // Both concrete subclasses on one chain → conflict → leave unbound (cleared to null).
        TriggerPoint rebound = TriggerMachineTypeRebinder.rebind(
                unbound,
                List.of(
                        "com.example.OrderService.pay()",
                        "com.example.DocumentService.submit()",
                        "com.example.SharedService.updateState()"),
                context);

        assertThat(rebound.getStateTypeFqn()).isNull();
        assertThat(rebound.getEventTypeFqn()).isNull();
    }

    @Test
    void infersEventTypeOwnerFromConcretePolymorphicEventsWhenStillUnbound(@TempDir Path tempDir) throws Exception {
        writeSharedBaseFixture(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        TriggerPoint unbound = TriggerPoint.builder()
                .className("com.example.SharedService")
                .methodName("updateState")
                .event("event")
                .stateTypeFqn("S")
                .eventTypeFqn("E")
                .polymorphicEvents(List.of("com.example.OrderEvent.PAY"))
                .build();

        // Chain has no subclass extends clause (controller only) → poly-owner fallback for event.
        TriggerPoint rebound = TriggerMachineTypeRebinder.rebind(
                unbound,
                List.of(
                        "com.example.OrderController.pay()",
                        "com.example.SharedService.updateState()"),
                context);

        assertThat(rebound.getEventTypeFqn()).isEqualTo("com.example.OrderEvent");
        assertThat(rebound.getStateTypeFqn()).isNull();
    }

    @Test
    void leavesConcreteTypesUntouched(@TempDir Path tempDir) throws Exception {
        writeSharedBaseFixture(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        TriggerPoint concrete = TriggerPoint.builder()
                .className("com.example.SharedService")
                .methodName("updateState")
                .event("com.example.OrderEvent.PAY")
                .stateTypeFqn("com.example.OrderState")
                .eventTypeFqn("com.example.OrderEvent")
                .build();

        TriggerPoint rebound = TriggerMachineTypeRebinder.rebind(
                concrete,
                List.of("com.example.DocumentService.submit()", "com.example.SharedService.updateState()"),
                context);

        assertThat(rebound).isSameAs(concrete);
    }

    private static void writeSharedBaseFixture(Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("Shared.java"), """
                package com.example;
                abstract class SharedService<S, E> {
                    org.springframework.statemachine.StateMachine<S, E> sm;
                    void updateState(E event) { sm.sendEvent(event); }
                }
                class OrderService extends SharedService<OrderState, OrderEvent> {
                    void pay() { updateState(OrderEvent.PAY); }
                }
                class DocumentService extends SharedService<DocumentState, DocumentEvent> {
                    void submit() { updateState(DocumentEvent.SUBMIT); }
                }
                class OrderController {
                    OrderService orderService;
                    void pay() { orderService.pay(); }
                }
                enum OrderEvent { PAY }
                enum OrderState { NEW, PAID }
                enum DocumentEvent { SUBMIT }
                enum DocumentState { DRAFT }
                """);
    }
}
