package click.kamil.springstatemachineexporter.ast.common;

import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import org.eclipse.jdt.core.dom.Expression;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

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

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

class FindLocalSetterArgumentTest {

    private CodebaseContext context;
    private VariableTracer variableTracer;

    @BeforeEach
    void setUp(@TempDir Path tempDir) throws IOException {
        writeJava(tempDir, "com/example/Payload.java", """
                package com.example;
                public class Payload {
                    private String event = "DEFAULT";
                    public String getEvent() { return event; }
                    public void setEvent(String event) { this.event = event; }
                }
                """);
        context = new CodebaseContext();
        context.setResolveBindings(true);
        context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
        variableTracer = new VariableTracer(context, context.getConstantResolver());
    }

    @Test
    void lastSetterBeforeGetterWins(@TempDir Path tempDir) throws IOException {
        writeJava(tempDir, "com/example/Runner.java", """
                package com.example;
                public class Runner {
                    public void run() {
                        Payload payload = new Payload();
                        payload.setEvent("FIRST");
                        payload.setEvent("SECOND");
                        String value = payload.getEvent();
                    }
                }
                """);
        context.scan(tempDir);

        Expression result = variableTracer.traceLocalSetter(
                "com.example.Runner.run", "payload", "getEvent");

        assertThat(result).isNotNull();
        assertThat(result.toString()).contains("SECOND");
    }

    @Test
    void setterAfterGetterIsIgnored(@TempDir Path tempDir) throws IOException {
        writeJava(tempDir, "com/example/Runner.java", """
                package com.example;
                public class Runner {
                    public void run() {
                        Payload payload = new Payload();
                        payload.setEvent("BEFORE");
                        String value = payload.getEvent();
                        payload.setEvent("AFTER");
                    }
                }
                """);
        context.scan(tempDir);

        Expression result = variableTracer.traceLocalSetter(
                "com.example.Runner.run", "payload", "getEvent");

        assertThat(result).isNotNull();
        assertThat(result.toString()).contains("BEFORE");
        assertThat(result.toString()).doesNotContain("AFTER");
    }

    @Test
    void ifElseBranchesBothReachGetter_lastOnCfgPathWins(@TempDir Path tempDir) throws IOException {
        writeJava(tempDir, "com/example/Runner.java", """
                package com.example;
                public class Runner {
                    public void run(boolean flag) {
                        Payload payload = new Payload();
                        if (flag) {
                            payload.setEvent("IF_BRANCH");
                        } else {
                            payload.setEvent("ELSE_BRANCH");
                        }
                        String value = payload.getEvent();
                    }
                }
                """);
        context.scan(tempDir);

        Expression result = variableTracer.traceLocalSetter(
                "com.example.Runner.run", "payload", "getEvent");

        assertThat(result).isNotNull();
        // Both branches are on the CFG path to the getter; last in source order wins.
        assertThat(result.toString()).contains("ELSE_BRANCH");
    }

    @Test
    void setterOnSiblingBranchIsExcluded(@TempDir Path tempDir) throws IOException {
        writeJava(tempDir, "com/example/Runner.java", """
                package com.example;
                public class Runner {
                    public void run(boolean flag) {
                        Payload payload = new Payload();
                        if (flag) {
                            payload.setEvent("TAKEN");
                            String value = payload.getEvent();
                            return;
                        }
                        payload.setEvent("OTHER_BRANCH");
                    }
                }
                """);
        context.scan(tempDir);

        Expression result = variableTracer.traceLocalSetter(
                "com.example.Runner.run", "payload", "getEvent");

        assertThat(result).isNotNull();
        assertThat(result.toString()).contains("TAKEN");
        assertThat(result.toString()).doesNotContain("OTHER_BRANCH");
    }

    @Test
    void noGetterCallFallsBackToOrderedScan(@TempDir Path tempDir) throws IOException {
        writeJava(tempDir, "com/example/Runner.java", """
                package com.example;
                public class Runner {
                    public void run() {
                        Payload payload = new Payload();
                        payload.setEvent("ONLY");
                    }
                }
                """);
        context.scan(tempDir);

        Expression result = variableTracer.traceLocalSetter(
                "com.example.Runner.run", "payload", "getEvent");

        assertThat(result).isNotNull();
        assertThat(result.toString()).contains("ONLY");
    }

    @Test
    void fieldAssignmentSetterIsFound(@TempDir Path tempDir) throws IOException {
        writeJava(tempDir, "com/example/MutablePayload.java", """
                package com.example;
                public class MutablePayload {
                    public String event;
                    public String getEvent() { return event; }
                }
                """);
        writeJava(tempDir, "com/example/Runner.java", """
                package com.example;
                public class Runner {
                    public void run() {
                        MutablePayload payload = new MutablePayload();
                        payload.event = "ASSIGNED";
                        String value = payload.getEvent();
                    }
                }
                """);
        context.scan(tempDir);

        Expression result = variableTracer.traceLocalSetter(
                "com.example.Runner.run", "payload", "getEvent");

        assertThat(result).isNotNull();
        assertThat(result.toString()).contains("ASSIGNED");
    }

    @Test
    void setterInsideNestedExpressionIsFound(@TempDir Path tempDir) throws IOException {
        writeJava(tempDir, "com/example/Runner.java", """
                package com.example;
                public class Runner {
                    public void run() {
                        Payload payload = new Payload();
                        consume(payload.setEvent("NESTED"));
                        String value = payload.getEvent();
                    }
                    private void consume(String ignored) {}
                }
                """);
        context.scan(tempDir);

        Expression result = variableTracer.traceLocalSetter(
                "com.example.Runner.run", "payload", "getEvent");

        assertThat(result).isNotNull();
        assertThat(result.toString()).contains("NESTED");
    }

    @Test
    void returnsNullWhenNoMatchingSetter(@TempDir Path tempDir) throws IOException {
        writeJava(tempDir, "com/example/Runner.java", """
                package com.example;
                public class Runner {
                    public void run() {
                        Payload payload = new Payload();
                        String value = payload.getEvent();
                    }
                }
                """);
        context.scan(tempDir);

        Expression result = variableTracer.traceLocalSetter(
                "com.example.Runner.run", "payload", "getEvent");

        assertThat(result).isNull();
    }

    private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
        Path file = tempDir.resolve(relativePath);
        Files.createDirectories(file.getParent());
        Files.writeString(file, source);
    }
}
