package com.xfestudio.xfeservermanager.core.trigger;

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 com.xfestudio.xfeservermanager.api.trigger.TriggerExtension;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;

class TriggerProgramV2Test {
    @Test
    void simulatesLoopsTypedExpressionsAndDeterministicRandomWithoutSideEffects() {
        TriggerExpression counter = TriggerExpression.reference("integer", "counter");
        TriggerExpression increment = new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.BINARY,
                "integer", null, "+", List.of(counter, TriggerExpression.literal("integer", 1L)));
        TriggerExpression random = new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.FUNCTION,
                "integer", null, "random_int", List.of(
                TriggerExpression.literal("integer", 1L), TriggerExpression.literal("integer", 100L)));
        TriggerProgramV2.Statement body = new TriggerProgramV2.Statement(UUID.randomUUID(),
                TriggerProgramV2.StatementKind.SET, "counter", Map.of(), increment,
                List.of(), List.of(), List.of());
        TriggerProgramV2.Statement action = new TriggerProgramV2.Statement(UUID.randomUUID(),
                TriggerProgramV2.StatementKind.ACTION, "log", Map.of("roll", random,
                "counter", TriggerExpression.reference("integer", "counter")),
                null, List.of(), List.of(), List.of());
        TriggerProgramV2 program = new TriggerProgramV2(2,
                List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.test", Map.of())),
                List.of(new TriggerProgramV2.VariableDeclaration(UUID.randomUUID(), "counter", "integer",
                        TriggerExpression.literal("integer", 0L), "trigger", "trigger",
                        TriggerProgramV2.Lifetime.PERSISTENT, null)), List.of(),
                List.of(new TriggerProgramV2.Statement(UUID.randomUUID(), TriggerProgramV2.StatementKind.REPEAT,
                        "", Map.of(), TriggerExpression.literal("integer", 3L), List.of(),
                        List.of(body, action), List.of())));

        TriggerSimulator.Request request = new TriggerSimulator.Request(
                Map.of("event.type", "custom.test"), Map.of(), 42L, Instant.EPOCH);
        TriggerSimulator.Result first = TriggerSimulator.simulate(program, request);
        TriggerSimulator.Result second = TriggerSimulator.simulate(program, request);

        assertEquals(TriggerSimulator.Status.COMPLETED, first.status());
        assertEquals(3L, first.variables().get("counter"));
        assertEquals(3, first.actions().size());
        assertEquals(first.actions().stream().map(TriggerSimulator.PlannedAction::parameters).toList(),
                second.actions().stream().map(TriggerSimulator.PlannedAction::parameters).toList());
    }

    @Test
    void booleanOperatorsShortCircuit() {
        TriggerExpression divisionByZero = new TriggerExpression(UUID.randomUUID(),
                TriggerExpression.Kind.BINARY, "float", null, "/", List.of(
                TriggerExpression.literal("integer", 1L), TriggerExpression.literal("integer", 0L)));
        TriggerExpression expression = new TriggerExpression(UUID.randomUUID(),
                TriggerExpression.Kind.BINARY, "bool", null, "and", List.of(
                TriggerExpression.literal("bool", false), divisionByZero));
        var evaluator = new TriggerExpressionEvaluator(Map.of(), new java.util.LinkedHashMap<>(),
                0L, Instant.EPOCH, null);

        assertFalse(evaluator.evaluateBoolean(expression));
    }

    @Test
    void legacyCompilationUsesStableNodeIdsAndNewBudgetsAreEnforced() {
        Instant now = Instant.EPOCH;
        TriggerDefinition legacy = new TriggerDefinition(UUID.randomUUID(), UUID.randomUUID(), "Legacy", "",
                true, TriggerDefinition.Mode.VISUAL,
                new TriggerDefinition.TriggerEvent("player.join", Map.of()),
                TriggerDefinition.MatchMode.ALL,
                List.of(new TriggerDefinition.Condition("player.name", "eq", "Steve")),
                List.of(new TriggerDefinition.Action("log", Map.of("message", "hello"))),
                "", 0, "owner", now, now, false);

        assertEquals(TriggerLegacyV2Compiler.compile(legacy), TriggerLegacyV2Compiler.compile(legacy));

        TriggerDefinition.Action nested = new TriggerDefinition.Action("log", Map.of("message", "end"));
        for (int index = 0; index < TriggerProgramValidator.MAX_NESTING_DEPTH; index++) {
            nested = new TriggerDefinition.Action("condition", Map.of(
                    "field", "event.type", "operator", "exists", "value", ""), List.of(nested));
        }
        TriggerDefinition.Action tooDeep = nested;
        assertThrows(IllegalArgumentException.class, () -> TriggerActionTree.validate(List.of(tooDeep)));
    }

    @Test
    void functionArgumentsUseCallerSnapshotAndOutParametersWriteBack() {
        TriggerProgramV2.Statement assignOutput = new TriggerProgramV2.Statement(UUID.randomUUID(),
                TriggerProgramV2.StatementKind.SET, "out", Map.of(),
                new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.BINARY,
                        "integer", null, "+", List.of(
                        TriggerExpression.reference("integer", "x"),
                        TriggerExpression.reference("integer", "y"))),
                List.of(), List.of(), List.of());
        TriggerProgramV2.FunctionDeclaration swapAdd = new TriggerProgramV2.FunctionDeclaration(
                UUID.randomUUID(), "swap_add", List.of(
                new TriggerProgramV2.Parameter("x", "integer", TriggerProgramV2.ParameterMode.IN),
                new TriggerProgramV2.Parameter("y", "integer", TriggerProgramV2.ParameterMode.IN),
                new TriggerProgramV2.Parameter("out", "integer", TriggerProgramV2.ParameterMode.OUT)),
                "void", List.of(), List.of(assignOutput));
        TriggerProgramV2.Statement call = new TriggerProgramV2.Statement(UUID.randomUUID(),
                TriggerProgramV2.StatementKind.CALL, "swap_add", Map.of(
                "x", TriggerExpression.reference("integer", "y"),
                "y", TriggerExpression.reference("integer", "x"),
                "out", TriggerExpression.reference("integer", "result")),
                null, List.of(), List.of(), List.of());
        TriggerProgramV2 program = new TriggerProgramV2(2,
                List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.test", Map.of())),
                List.of(variable("x", 2L), variable("y", 3L), variable("result", 0L)),
                List.of(swapAdd), List.of(call));

        TriggerSimulator.Result result = TriggerSimulator.simulate(program,
                new TriggerSimulator.Request(Map.of(), Map.of(), 1L, Instant.EPOCH));

        assertEquals(TriggerSimulator.Status.COMPLETED, result.status());
        assertEquals(5L, result.variables().get("result"));
    }

    @Test
    void rejectsStaticTypeMismatchesBeforeExecution() {
        assertThrows(IllegalArgumentException.class, () -> new TriggerProgramV2(2,
                List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.test", Map.of())),
                List.of(variable("counter", 0L)), List.of(),
                List.of(new TriggerProgramV2.Statement(UUID.randomUUID(),
                        TriggerProgramV2.StatementKind.SET, "counter", Map.of(),
                        TriggerExpression.literal("string", "wrong"),
                        List.of(), List.of(), List.of()))));
    }

    @Test
    void extensionsMayRaiseButNeverLowerBuiltInRisk() {
        TriggerExtension.Descriptor builtIn = TriggerDescriptorCatalog.DESCRIPTORS.stream()
                .filter(value -> value.kind() == TriggerExtension.Kind.ACTION
                        && value.id().equals("server_command"))
                .findFirst().orElseThrow();
        Map<String, TriggerExtension.Descriptor> descriptors = new LinkedHashMap<>();
        descriptors.put("ACTION:server_command", builtIn);
        TriggerExtension extension = new TriggerExtension() {
            @Override public String namespace() { return "example"; }
            @Override public String version() { return "1.0.0"; }
            @Override public List<Descriptor> descriptors() {
                return List.of(new Descriptor("server_command", Kind.ACTION, "command", "",
                        List.of(), "void", Purity.SIDE_EFFECT, ThreadAffinity.SERVER,
                        Risk.SAFE, Set.of("forge-1.20.1"), Set.of("*"), Map.of()));
            }
        };

        TriggerExtensionRegistry.register(extension, descriptors);

        assertEquals(TriggerExtension.Risk.OWNER,
                descriptors.get("ACTION:server_command").risk());
    }

    @Test
    void extensionsBindOnlyNamespacedTransportSafeFunctionAndActionHandlers() throws Exception {
        Map<String, TriggerExtension.Descriptor> descriptors = new LinkedHashMap<>();
        Map<String, TriggerExtension.FunctionHandler> functions = new LinkedHashMap<>();
        Map<String, TriggerExtension.ActionHandler> actions = new LinkedHashMap<>();
        java.util.concurrent.atomic.AtomicReference<String> actionKey =
                new java.util.concurrent.atomic.AtomicReference<>();
        TriggerExtension extension = new TriggerExtension() {
            @Override public String namespace() { return "example"; }
            @Override public String version() { return "1.0.0"; }
            @Override public List<Descriptor> descriptors() {
                return List.of(
                        new Descriptor("example.double", Kind.VALUE_FUNCTION, "double", "",
                                List.of(new Parameter("value", "integer", true, null, "")),
                                "integer", Purity.PURE, ThreadAffinity.BACKGROUND, Risk.SAFE,
                                Set.of("forge-1.20.1"), Set.of("*"), Map.of()),
                        new Descriptor("example.audit", Kind.ACTION, "audit", "", List.of(),
                                "void", Purity.SIDE_EFFECT, ThreadAffinity.SERVER, Risk.ADMINISTRATOR,
                                Set.of("forge-1.20.1"), Set.of("*"), Map.of()));
            }
            @Override public Map<String, FunctionHandler> functionHandlers() {
                return Map.of("example.double", invocation ->
                        ((Number) invocation.arguments().get(0)).longValue() * 2L);
            }
            @Override public Map<String, ActionHandler> actionHandlers() {
                return Map.of("example.audit", invocation -> actionKey.set(invocation.idempotencyKey()));
            }
        };

        TriggerExtensionRegistry.register(extension, descriptors, functions, actions);

        assertEquals(8L, functions.get("example.double").evaluate(
                new TriggerExtension.FunctionInvocation("example.double", List.of(4L), Map.of(), Map.of(),
                        1L, Instant.EPOCH.toString())));
        actions.get("example.audit").execute(new TriggerExtension.ActionInvocation(
                "example.audit", Map.of(), Map.of(), "execution", UUID.randomUUID().toString(), "key"));
        assertEquals("key", actionKey.get());
    }

    @Test
    void liveVmResumesAfterWaitWithoutRepeatingEffectsOrRandomValues() {
        TriggerProgramV2.Statement before = action("log", Map.of("message", randomRoll()));
        TriggerProgramV2.Statement wait = action("wait", Map.of(
                "mode", TriggerExpression.literal("string", "duration"),
                "value", TriggerExpression.literal("string", "1")));
        TriggerProgramV2.Statement after = action("log", Map.of("message", randomRoll()));
        TriggerProgramV2 program = new TriggerProgramV2(2,
                List.of(new TriggerProgramV2.EventBinding(UUID.randomUUID(), "custom.resume", Map.of())),
                List.of(), List.of(), List.of(before, wait, after));

        TriggerProgramExecutor.State started = TriggerProgramExecutor.start(
                program, Map.of("event.type", "custom.resume"), Map.of(), 77L, Instant.EPOCH);
        TriggerProgramExecutor.Result waiting = TriggerProgramExecutor.resume(program, started, 64);
        TriggerProgramExecutor.Result completed = TriggerProgramExecutor.resume(program, waiting.state(), 64);
        TriggerProgramExecutor.Result replayWaiting = TriggerProgramExecutor.resume(program,
                TriggerProgramExecutor.start(program, Map.of("event.type", "custom.resume"),
                        Map.of(), 77L, Instant.EPOCH), 64);
        TriggerProgramExecutor.Result replayCompleted = TriggerProgramExecutor.resume(
                program, replayWaiting.state(), 64);

        assertEquals(TriggerProgramExecutor.Status.WAITING, waiting.status());
        assertEquals(List.of("log", "wait"), waiting.effects().stream()
                .map(TriggerProgramExecutor.Effect::type).toList());
        assertEquals(TriggerProgramExecutor.Status.COMPLETED, completed.status());
        assertEquals(List.of("log"), completed.effects().stream()
                .map(TriggerProgramExecutor.Effect::type).toList());
        assertEquals(waiting.effects().get(0).parameters(), replayWaiting.effects().get(0).parameters());
        assertEquals(completed.effects().get(0).parameters(), replayCompleted.effects().get(0).parameters());
        assertEquals(2L, completed.state().randomCounter());
        assertEquals(3, java.util.stream.Stream.concat(waiting.effects().stream(), completed.effects().stream())
                .map(TriggerProgramExecutor.Effect::idempotencyKey).distinct().count());
    }

    private static TriggerProgramV2.Statement action(
            String name, Map<String, TriggerExpression> inputs) {
        return new TriggerProgramV2.Statement(UUID.randomUUID(), TriggerProgramV2.StatementKind.ACTION,
                name, inputs, null, List.of(), List.of(), List.of());
    }

    private static TriggerExpression randomRoll() {
        return new TriggerExpression(UUID.randomUUID(), TriggerExpression.Kind.FUNCTION,
                "integer", null, "random_int", List.of(
                TriggerExpression.literal("integer", 1L), TriggerExpression.literal("integer", 1_000L)));
    }

    private static TriggerProgramV2.VariableDeclaration variable(String name, long initial) {
        return new TriggerProgramV2.VariableDeclaration(UUID.randomUUID(), name, "integer",
                TriggerExpression.literal("integer", initial), "trigger", "trigger",
                TriggerProgramV2.Lifetime.SESSION, null);
    }
}
