package com.cameleer.server.core.runtime;

import com.cameleer.common.model.ApplicationConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;
import java.util.UUID;

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

class DirtyStateCalculatorTest {

    private static final DirtyStateCalculator CALC = new DirtyStateCalculator(
            new ObjectMapper().registerModule(new JavaTimeModule()));

    @Test
    void noSnapshot_meansEverythingDirty() {
        DirtyStateCalculator calc = CALC;

        ApplicationConfig desiredAgent = new ApplicationConfig();
        desiredAgent.setSamplingRate(1.0);
        Map<String, Object> desiredContainer = Map.of("memoryLimitMb", 512);

        DirtyStateResult result = calc.compute(UUID.randomUUID(), desiredAgent, desiredContainer, null);

        assertThat(result.dirty()).isTrue();
        assertThat(result.differences()).extracting(DirtyStateResult.Difference::field)
                .contains("snapshot");
    }

    @Test
    void identicalSnapshot_isClean() {
        DirtyStateCalculator calc = CALC;

        ApplicationConfig cfg = new ApplicationConfig();
        cfg.setSamplingRate(0.5);
        Map<String, Object> container = Map.of("memoryLimitMb", 512);

        UUID jarId = UUID.randomUUID();
        DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, cfg, container, null);
        DirtyStateResult result = calc.compute(jarId, cfg, container, snap);

        assertThat(result.dirty()).isFalse();
        assertThat(result.differences()).isEmpty();
    }

    @Test
    void differentJar_marksJarField() {
        DirtyStateCalculator calc = CALC;
        ApplicationConfig cfg = new ApplicationConfig();
        Map<String, Object> container = Map.of();
        UUID v1 = UUID.randomUUID();
        UUID v2 = UUID.randomUUID();
        DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(v1, cfg, container, null);

        DirtyStateResult result = calc.compute(v2, cfg, container, snap);

        assertThat(result.dirty()).isTrue();
        assertThat(result.differences()).extracting(DirtyStateResult.Difference::field)
                .contains("jarVersionId");
    }

    @Test
    void differentSamplingRate_marksAgentField() {
        DirtyStateCalculator calc = CALC;

        ApplicationConfig deployedCfg = new ApplicationConfig();
        deployedCfg.setSamplingRate(0.5);
        ApplicationConfig desiredCfg = new ApplicationConfig();
        desiredCfg.setSamplingRate(1.0);
        UUID jarId = UUID.randomUUID();
        DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, deployedCfg, Map.of(), null);

        DirtyStateResult result = calc.compute(jarId, desiredCfg, Map.of(), snap);

        assertThat(result.dirty()).isTrue();
        assertThat(result.differences()).extracting(DirtyStateResult.Difference::field)
                .contains("agentConfig.samplingRate");
    }

    @Test
    void differentContainerMemory_marksContainerField() {
        DirtyStateCalculator calc = CALC;
        ApplicationConfig cfg = new ApplicationConfig();
        UUID jarId = UUID.randomUUID();
        DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, cfg, Map.of("memoryLimitMb", 512), null);

        DirtyStateResult result = calc.compute(jarId, cfg, Map.of("memoryLimitMb", 1024), snap);

        assertThat(result.dirty()).isTrue();
        assertThat(result.differences()).extracting(DirtyStateResult.Difference::field)
                .contains("containerConfig.memoryLimitMb");
    }

    @Test
    void nullAgentConfigInSnapshot_marksAgentConfigDiff() {
        DirtyStateCalculator calc = CALC;
        ApplicationConfig desired = new ApplicationConfig();
        desired.setSamplingRate(1.0);
        UUID jarId = UUID.randomUUID();
        DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, null, Map.of(), null);

        DirtyStateResult result = calc.compute(jarId, desired, Map.of(), snap);

        assertThat(result.dirty()).isTrue();
        assertThat(result.differences()).extracting(DirtyStateResult.Difference::field)
                .contains("agentConfig");
    }

    @Test
    void nestedAgentField_reportsDeepPath() {
        DirtyStateCalculator calc = CALC;

        ApplicationConfig deployed = new ApplicationConfig();
        deployed.setSensitiveKeys(List.of("password", "token"));
        ApplicationConfig desired = new ApplicationConfig();
        desired.setSensitiveKeys(List.of("password", "token", "secret"));
        UUID jarId = UUID.randomUUID();
        DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, deployed, Map.of(), null);

        DirtyStateResult result = calc.compute(jarId, desired, Map.of(), snap);

        assertThat(result.dirty()).isTrue();
        assertThat(result.differences()).extracting(DirtyStateResult.Difference::field)
                .anyMatch(f -> f.startsWith("agentConfig.sensitiveKeys"));
    }

    @Test
    void livePushedFields_doNotMarkDirty() {
        // Taps, tracedProcessors, and routeRecording apply via live SSE push (never redeploy),
        // so they must not appear as "pending deploy" when they differ from the last deploy snapshot.
        ApplicationConfig deployed = new ApplicationConfig();
        deployed.setTracedProcessors(Map.of("proc-1", "DEBUG"));
        deployed.setRouteRecording(Map.of("route-a", true));
        deployed.setTapVersion(1);

        ApplicationConfig desired = new ApplicationConfig();
        desired.setTracedProcessors(Map.of("proc-1", "TRACE", "proc-2", "DEBUG"));
        desired.setRouteRecording(Map.of("route-a", false, "route-b", true));
        desired.setTapVersion(5);

        UUID jarId = UUID.randomUUID();
        DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, deployed, Map.of(), null);
        DirtyStateResult result = CALC.compute(jarId, desired, Map.of(), snap);

        assertThat(result.dirty()).isFalse();
        assertThat(result.differences()).isEmpty();
    }

    @Test
    void stringField_differenceValueIsUnquoted() {
        DirtyStateCalculator calc = CALC;

        ApplicationConfig deployed = new ApplicationConfig();
        deployed.setApplicationLogLevel("INFO");
        ApplicationConfig desired = new ApplicationConfig();
        desired.setApplicationLogLevel("DEBUG");
        UUID jarId = UUID.randomUUID();
        DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, deployed, Map.of(), null);

        DirtyStateResult result = calc.compute(jarId, desired, Map.of(), snap);

        DirtyStateResult.Difference diff = result.differences().stream()
                .filter(d -> d.field().equals("agentConfig.applicationLogLevel"))
                .findFirst().orElseThrow();
        assertThat(diff.staged()).isEqualTo("DEBUG");
        assertThat(diff.deployed()).isEqualTo("INFO");
    }

    @Test
    void versionBumpDoesNotMarkDirty() {
        ApplicationConfig deployedCfg = new ApplicationConfig();
        deployedCfg.setSamplingRate(0.5);
        deployedCfg.setVersion(1);
        ApplicationConfig desiredCfg = new ApplicationConfig();
        desiredCfg.setSamplingRate(0.5);
        desiredCfg.setVersion(2);  // bumped by save
        UUID jarId = UUID.randomUUID();
        DeploymentConfigSnapshot snap = new DeploymentConfigSnapshot(jarId, deployedCfg, Map.of(), null);

        DirtyStateResult result = CALC.compute(jarId, desiredCfg, Map.of(), snap);
        assertThat(result.dirty()).isFalse();
    }
}
