package com.cameleer.server.app.controller;

import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper;
import com.cameleer.server.app.dto.DirtyStateResponse;
import com.cameleer.server.app.storage.PostgresDeploymentRepository;
import com.cameleer.server.core.runtime.ContainerStatus;
import com.cameleer.server.core.runtime.Deployment;
import com.cameleer.server.core.runtime.DeploymentStatus;
import com.cameleer.server.core.runtime.RuntimeOrchestrator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

/**
 * Integration tests for GET /api/v1/environments/{envSlug}/apps/{appSlug}/dirty-state.
 *
 * <p>Uses @MockBean RuntimeOrchestrator (same pattern as DeploymentSnapshotIT).
 * @DirtiesContext prevents context cache conflicts when both IT classes are loaded together.</p>
 */
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
class AppDirtyStateIT extends AbstractPostgresIT {

    @MockBean
    RuntimeOrchestrator runtimeOrchestrator;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private TestSecurityHelper securityHelper;

    @Autowired
    private PostgresDeploymentRepository deploymentRepository;

    private String operatorJwt;

    @BeforeEach
    void setUp() {
        operatorJwt = securityHelper.operatorToken();
        jdbcTemplate.update("DELETE FROM deployments");
        jdbcTemplate.update("DELETE FROM app_versions");
        jdbcTemplate.update("DELETE FROM apps");
        jdbcTemplate.update("DELETE FROM application_config WHERE environment = 'default'");

        // Ensure test-operator exists in users table (required for deployments.created_by FK)
        jdbcTemplate.update(
                "INSERT INTO users (user_id, provider, display_name) VALUES ('test-operator', 'local', 'Test Operator') ON CONFLICT (user_id) DO NOTHING");
    }

    // -----------------------------------------------------------------------
    // Test 1: no deployment ever → dirty=true, lastSuccessfulDeploymentId=null
    // -----------------------------------------------------------------------

    @Test
    void dirtyState_noDeployEver_returnsDirtyTrue() throws Exception {
        String appSlug = "ds-nodeploy-" + UUID.randomUUID().toString().substring(0, 8);
        post("/api/v1/environments/default/apps",
                String.format("{\"slug\": \"%s\", \"displayName\": \"DS No Deploy\"}", appSlug),
                operatorJwt);
        uploadJar(appSlug, ("fake-jar-" + appSlug).getBytes());
        put("/api/v1/environments/default/apps/" + appSlug + "/config",
                "{\"samplingRate\": 0.5}", operatorJwt);

        DirtyStateResponse body = getDirtyState("default", appSlug);

        assertThat(body.dirty()).isTrue();
        assertThat(body.lastSuccessfulDeploymentId()).isNull();
    }

    // -----------------------------------------------------------------------
    // Test 2: after a successful deploy with matching desired state → dirty=false
    // -----------------------------------------------------------------------

    @Test
    void dirtyState_afterSuccessfulDeploy_matchingDesiredState_returnsDirtyFalse() throws Exception {
        String fakeContainerId = "fake-cid-" + UUID.randomUUID();
        when(runtimeOrchestrator.isEnabled()).thenReturn(true);
        when(runtimeOrchestrator.startContainer(any())).thenReturn(fakeContainerId);
        when(runtimeOrchestrator.getContainerStatus(fakeContainerId))
                .thenReturn(new ContainerStatus("healthy", true, 0, null));

        String appSlug = "ds-clean-" + UUID.randomUUID().toString().substring(0, 8);
        post("/api/v1/environments/default/apps",
                String.format("{\"slug\": \"%s\", \"displayName\": \"DS Clean\"}", appSlug),
                operatorJwt);
        put("/api/v1/environments/default/apps/" + appSlug + "/container-config",
                "{\"runtimeType\": \"spring-boot\", \"appPort\": 8081}", operatorJwt);
        String versionId = uploadJar(appSlug, ("fake-jar-clean-" + appSlug).getBytes());
        put("/api/v1/environments/default/apps/" + appSlug + "/config",
                "{\"samplingRate\": 0.25}", operatorJwt);

        // Deploy and wait for RUNNING
        JsonNode deploy = post(
                "/api/v1/environments/default/apps/" + appSlug + "/deployments",
                String.format("{\"appVersionId\": \"%s\"}", versionId),
                operatorJwt);
        String deploymentId = deploy.path("id").asText();

        await().atMost(30, TimeUnit.SECONDS).pollInterval(500, TimeUnit.MILLISECONDS)
                .untilAsserted(() -> {
                    Deployment d = deploymentRepository.findById(UUID.fromString(deploymentId))
                            .orElseThrow(() -> new AssertionError("Deployment not found"));
                    assertThat(d.status()).isEqualTo(DeploymentStatus.RUNNING);
                });

        // Desired state matches what was deployed → dirty=false
        DirtyStateResponse body = getDirtyState("default", appSlug);

        assertThat(body.dirty()).isFalse();
        assertThat(body.differences()).isEmpty();
        assertThat(body.lastSuccessfulDeploymentId()).isEqualTo(deploymentId);
    }

    // -----------------------------------------------------------------------
    // Test 3: after successful deploy, config changed → dirty=true
    // -----------------------------------------------------------------------

    @Test
    void dirtyState_afterSuccessfulDeploy_configChanged_returnsDirtyTrue() throws Exception {
        String fakeContainerId = "fake-cid2-" + UUID.randomUUID();
        when(runtimeOrchestrator.isEnabled()).thenReturn(true);
        when(runtimeOrchestrator.startContainer(any())).thenReturn(fakeContainerId);
        when(runtimeOrchestrator.getContainerStatus(fakeContainerId))
                .thenReturn(new ContainerStatus("healthy", true, 0, null));

        String appSlug = "ds-dirty-" + UUID.randomUUID().toString().substring(0, 8);
        post("/api/v1/environments/default/apps",
                String.format("{\"slug\": \"%s\", \"displayName\": \"DS Dirty\"}", appSlug),
                operatorJwt);
        put("/api/v1/environments/default/apps/" + appSlug + "/container-config",
                "{\"runtimeType\": \"spring-boot\", \"appPort\": 8081}", operatorJwt);
        String versionId = uploadJar(appSlug, ("fake-jar-dirty-" + appSlug).getBytes());
        put("/api/v1/environments/default/apps/" + appSlug + "/config",
                "{\"samplingRate\": 0.1}", operatorJwt);

        // Deploy and wait for RUNNING
        JsonNode deploy = post(
                "/api/v1/environments/default/apps/" + appSlug + "/deployments",
                String.format("{\"appVersionId\": \"%s\"}", versionId),
                operatorJwt);
        String deploymentId = deploy.path("id").asText();

        await().atMost(30, TimeUnit.SECONDS).pollInterval(500, TimeUnit.MILLISECONDS)
                .untilAsserted(() -> {
                    Deployment d = deploymentRepository.findById(UUID.fromString(deploymentId))
                            .orElseThrow(() -> new AssertionError("Deployment not found"));
                    assertThat(d.status()).isEqualTo(DeploymentStatus.RUNNING);
                });

        // Change samplingRate after deploy
        put("/api/v1/environments/default/apps/" + appSlug + "/config",
                "{\"samplingRate\": 0.9}", operatorJwt);

        // Now desired state differs from snapshot → dirty=true
        DirtyStateResponse body = getDirtyState("default", appSlug);

        assertThat(body.dirty()).isTrue();
        assertThat(body.lastSuccessfulDeploymentId()).isEqualTo(deploymentId);
        assertThat(body.differences()).isNotEmpty();
        assertThat(body.differences())
                .anyMatch(d -> d.field().contains("samplingRate"));
    }

    // -----------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------

    private DirtyStateResponse getDirtyState(String envSlug, String appSlug) {
        HttpHeaders headers = securityHelper.authHeaders(operatorJwt);
        var response = restTemplate.exchange(
                "/api/v1/environments/" + envSlug + "/apps/" + appSlug + "/dirty-state",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                DirtyStateResponse.class);
        assertThat(response.getStatusCode().value()).isEqualTo(200);
        return response.getBody();
    }

    private JsonNode post(String path, String json, String jwt) throws Exception {
        HttpHeaders headers = securityHelper.authHeaders(jwt);
        var response = restTemplate.exchange(
                path, HttpMethod.POST,
                new HttpEntity<>(json, headers),
                String.class);
        return objectMapper.readTree(response.getBody());
    }

    private void put(String path, String json, String jwt) {
        HttpHeaders headers = securityHelper.authHeaders(jwt);
        restTemplate.exchange(path, HttpMethod.PUT, new HttpEntity<>(json, headers), String.class);
    }

    private String uploadJar(String appSlug, byte[] content) throws Exception {
        ByteArrayResource resource = new ByteArrayResource(content) {
            @Override
            public String getFilename() { return "app.jar"; }
        };
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        body.add("file", resource);

        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + operatorJwt);
        headers.set("X-Cameleer-Protocol-Version", "1");
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);

        var response = restTemplate.exchange(
                "/api/v1/environments/default/apps/" + appSlug + "/versions",
                HttpMethod.POST,
                new HttpEntity<>(body, headers),
                String.class);

        JsonNode versionNode = objectMapper.readTree(response.getBody());
        return versionNode.path("id").asText();
    }
}
