package com.cameleer.server.app.runtime;

import com.cameleer.common.model.ApplicationConfig;
import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper;
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.context.TestPropertySource;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

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

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;

/**
 * Verifies that DeploymentExecutor writes DeploymentConfigSnapshot on successful
 * RUNNING transition and does NOT write it on a FAILED path (both the
 * startContainer-throws path and the health-check-fails path).
 */
@TestPropertySource(properties = "cameleer.server.runtime.healthchecktimeout=2")
class DeploymentSnapshotIT extends AbstractPostgresIT {

    @MockBean
    RuntimeOrchestrator runtimeOrchestrator;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private TestSecurityHelper securityHelper;

    @Autowired
    private PostgresDeploymentRepository deploymentRepository;

    private String operatorJwt;
    private String adminJwt;

    @BeforeEach
    void setUp() throws Exception {
        operatorJwt = securityHelper.operatorToken();
        adminJwt = securityHelper.adminToken();

        // Clean up between tests
        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: snapshot is populated when deployment reaches RUNNING
    // -----------------------------------------------------------------------

    @Test
    void snapshot_isPopulated_whenDeploymentReachesRunning() throws Exception {
        // --- given: mock orchestrator that simulates a healthy single-replica container ---
        String fakeContainerId = "fake-container-" + UUID.randomUUID();

        when(runtimeOrchestrator.isEnabled()).thenReturn(true);
        when(runtimeOrchestrator.startContainer(any()))
                .thenReturn(fakeContainerId);
        when(runtimeOrchestrator.getContainerStatus(fakeContainerId))
                .thenReturn(new ContainerStatus("healthy", true, 0, null));

        // --- given: create app with explicit runtimeType so auto-detection is not needed ---
        String appSlug = "snap-success-" + UUID.randomUUID().toString().substring(0, 8);
        String containerConfigJson = """
                {"runtimeType": "spring-boot", "appPort": 8081}
                """;
        String createAppJson = String.format("""
                {"slug": "%s", "displayName": "Snapshot Success App"}
                """, appSlug);

        JsonNode createdApp = post("/api/v1/environments/default/apps", createAppJson, operatorJwt);
        String appId = createdApp.path("id").asText();

        // --- given: update containerConfig to set runtimeType ---
        put("/api/v1/environments/default/apps/" + appSlug + "/container-config",
                containerConfigJson, operatorJwt);

        // --- given: upload a JAR (fake bytes; real file written to disk by AppService) ---
        String versionId = uploadJar(appSlug, ("fake-jar-bytes-" + appSlug).getBytes());

        // --- given: save agentConfig with samplingRate = 0.25 ---
        String configJson = """
                {"samplingRate": 0.25}
                """;
        put("/api/v1/environments/default/apps/" + appSlug + "/config", configJson, operatorJwt);

        // --- when: trigger deploy ---
        String deployJson = String.format("""
                {"appVersionId": "%s"}
                """, versionId);
        JsonNode deployResponse = post(
                "/api/v1/environments/default/apps/" + appSlug + "/deployments",
                deployJson, operatorJwt);
        String deploymentId = deployResponse.path("id").asText();

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

        // --- then: snapshot is populated ---
        Deployment deployed = deploymentRef.get();
        assertThat(deployed.deployedConfigSnapshot()).isNotNull();
        assertThat(deployed.deployedConfigSnapshot().jarVersionId())
                .isEqualTo(UUID.fromString(versionId));
        assertThat(deployed.deployedConfigSnapshot().agentConfig()).isNotNull();
        assertThat(deployed.deployedConfigSnapshot().agentConfig().getSamplingRate())
                .isEqualTo(0.25);
        assertThat(deployed.deployedConfigSnapshot().containerConfig())
                .containsEntry("runtimeType", "spring-boot")
                .containsEntry("appPort", 8081);
    }

    // -----------------------------------------------------------------------
    // Test 2: snapshot is NOT populated when deployment fails
    // -----------------------------------------------------------------------

    @Test
    void snapshot_isNotPopulated_whenDeploymentFails() throws Exception {
        // --- given: mock orchestrator that throws on startContainer ---
        when(runtimeOrchestrator.isEnabled()).thenReturn(true);
        when(runtimeOrchestrator.startContainer(any()))
                .thenThrow(new RuntimeException("Simulated container start failure"));

        // --- given: create app with explicit runtimeType ---
        String appSlug = "snap-fail-" + UUID.randomUUID().toString().substring(0, 8);
        String createAppJson = String.format("""
                {"slug": "%s", "displayName": "Snapshot Fail App"}
                """, appSlug);
        post("/api/v1/environments/default/apps", createAppJson, operatorJwt);

        put("/api/v1/environments/default/apps/" + appSlug + "/container-config",
                """
                {"runtimeType": "spring-boot", "appPort": 8081}
                """, operatorJwt);

        String versionId = uploadJar(appSlug, ("fake-jar-fail-" + appSlug).getBytes());

        // --- when: trigger deploy ---
        String deployJson = String.format("""
                {"appVersionId": "%s"}
                """, versionId);
        JsonNode deployResponse = post(
                "/api/v1/environments/default/apps/" + appSlug + "/deployments",
                deployJson, operatorJwt);
        String deploymentId = deployResponse.path("id").asText();

        // --- await FAILED (async executor catches exception and marks failed) ---
        await().atMost(30, TimeUnit.SECONDS)
                .pollInterval(500, TimeUnit.MILLISECONDS)
                .untilAsserted(() -> {
                    Deployment d = deploymentRepository.findById(UUID.fromString(deploymentId))
                            .orElseThrow(() -> new AssertionError("Deployment not found: " + deploymentId));
                    assertThat(d.status()).isEqualTo(DeploymentStatus.FAILED);
                });

        // --- then: snapshot is null ---
        Deployment failed = deploymentRepository.findById(UUID.fromString(deploymentId)).orElseThrow();
        assertThat(failed.deployedConfigSnapshot()).isNull();
    }

    // -----------------------------------------------------------------------
    // Test 3: snapshot is NOT populated when the health check never passes.
    // This exercises the early-exit path in DeploymentExecutor (line ~231) —
    // startContainer succeeds, but no replica ever reports healthy, so
    // waitForAnyHealthy returns 0 before the snapshot-write point.
    // -----------------------------------------------------------------------

    @Test
    void snapshot_isNotPopulated_whenHealthCheckFails() throws Exception {
        // --- given: container starts but never becomes healthy ---
        String fakeContainerId = "fake-unhealthy-" + UUID.randomUUID();

        when(runtimeOrchestrator.isEnabled()).thenReturn(true);
        when(runtimeOrchestrator.startContainer(any())).thenReturn(fakeContainerId);
        when(runtimeOrchestrator.getContainerStatus(fakeContainerId))
                .thenReturn(new ContainerStatus("starting", true, 0, null));

        String appSlug = "snap-unhealthy-" + UUID.randomUUID().toString().substring(0, 8);
        post("/api/v1/environments/default/apps", String.format("""
                {"slug": "%s", "displayName": "Snapshot Unhealthy App"}
                """, appSlug), operatorJwt);
        put("/api/v1/environments/default/apps/" + appSlug + "/container-config",
                """
                {"runtimeType": "spring-boot", "appPort": 8081}
                """, operatorJwt);
        String versionId = uploadJar(appSlug, ("fake-jar-unhealthy-" + appSlug).getBytes());

        // --- when: trigger deploy ---
        JsonNode deployResponse = post(
                "/api/v1/environments/default/apps/" + appSlug + "/deployments",
                String.format("{\"appVersionId\": \"%s\"}", versionId), operatorJwt);
        String deploymentId = deployResponse.path("id").asText();

        // --- await FAILED (healthchecktimeout overridden to 2s in @TestPropertySource) ---
        await().atMost(30, TimeUnit.SECONDS)
                .pollInterval(500, TimeUnit.MILLISECONDS)
                .untilAsserted(() -> {
                    Deployment d = deploymentRepository.findById(UUID.fromString(deploymentId))
                            .orElseThrow(() -> new AssertionError("Deployment not found: " + deploymentId));
                    assertThat(d.status()).isEqualTo(DeploymentStatus.FAILED);
                });

        // --- then: snapshot is null (snapshot-write is gated behind health check) ---
        Deployment failed = deploymentRepository.findById(UUID.fromString(deploymentId)).orElseThrow();
        assertThat(failed.deployedConfigSnapshot()).isNull();
    }

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

    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();
    }
}
