package com.cameleer.server.app.controller;

import com.cameleer.common.model.ApplicationConfig;
import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper;
import com.cameleer.server.app.storage.PostgresApplicationConfigRepository;
import com.cameleer.server.core.agent.AgentRegistryService;
import com.cameleer.server.core.agent.CommandType;
import org.junit.jupiter.api.AfterEach;
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.SpyBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
class ApplicationConfigControllerIT extends AbstractPostgresIT {

    /**
     * Spy on the real AgentRegistryService bean so we can verify whether
     * addGroupCommandWithReplies was invoked (live) or skipped (staged).
     */
    @SpyBean
    AgentRegistryService registryService;

    @Autowired private TestRestTemplate restTemplate;
    @Autowired private TestSecurityHelper securityHelper;
    @Autowired private PostgresApplicationConfigRepository configRepository;

    private String operatorJwt;
    /** Unique env slug per test to avoid cross-test pollution. */
    private String envSlug;
    private UUID envId;
    /** Unique app slug per test run to avoid cross-test row collisions. */
    private String appSlug;

    @BeforeEach
    void setUp() {
        operatorJwt = securityHelper.operatorToken();
        envSlug  = "cfg-it-" + UUID.randomUUID().toString().substring(0, 8);
        envId    = UUID.randomUUID();
        appSlug  = "paygw-" + UUID.randomUUID().toString().substring(0, 8);

        jdbcTemplate.update(
                "INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?) ON CONFLICT (id) DO NOTHING",
                envId, envSlug, envSlug);
    }

    @AfterEach
    void cleanUp() {
        jdbcTemplate.update("DELETE FROM application_config WHERE environment = ?", envSlug);
        jdbcTemplate.update("DELETE FROM environments WHERE id = ?", envId);
    }

    // ── helpers ──────────────────────────────────────────────────────────────

    private void registerLiveAgent(String agentId) {
        // Use the bootstrap HTTP endpoint — same pattern as AgentCommandControllerIT.
        String body = """
                {
                  "instanceId": "%s",
                  "applicationId": "%s",
                  "environmentId": "%s",
                  "version": "1.0.0",
                  "routeIds": ["route-1"],
                  "capabilities": {}
                }
                """.formatted(agentId, appSlug, envSlug);
        restTemplate.postForEntity(
                "/api/v1/agents/register",
                new HttpEntity<>(body, securityHelper.bootstrapHeaders()),
                String.class);
    }

    private ResponseEntity<String> putConfig(String apply) {
        String url = "/api/v1/environments/" + envSlug + "/apps/" + appSlug + "/config"
                + (apply != null ? "?apply=" + apply : "");
        String body = """
                {"samplingRate": 0.1, "metricsEnabled": true}
                """;
        return restTemplate.exchange(url, HttpMethod.PUT,
                new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)), String.class);
    }

    // ── tests ─────────────────────────────────────────────────────────────────

    @Test
    void putConfig_staged_savesButDoesNotPush() {
        // Given — one LIVE agent registered for (appSlug, envSlug)
        String agentId = "staged-agent-" + UUID.randomUUID().toString().substring(0, 8);
        registerLiveAgent(agentId);

        // When — PUT with apply=staged
        ResponseEntity<String> response = putConfig("staged");

        // Then — HTTP 200
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        // And — DB has the new config
        ApplicationConfig saved = configRepository
                .findByApplicationAndEnvironment(appSlug, envSlug)
                .orElseThrow(() -> new AssertionError("Config not found in DB"));
        assertThat(saved.getSamplingRate()).isEqualTo(0.1);

        // And — NO CONFIG_UPDATE was pushed to any agent
        verify(registryService, never())
                .addGroupCommandWithReplies(eq(appSlug), eq(envSlug), eq(CommandType.CONFIG_UPDATE), any());
    }

    @Test
    void putConfig_live_savesAndPushes() {
        // Given — one LIVE agent registered for (appSlug, envSlug)
        String agentId = "live-agent-" + UUID.randomUUID().toString().substring(0, 8);
        registerLiveAgent(agentId);

        // When — PUT without apply param (default is live)
        ResponseEntity<String> response = putConfig(null);

        // Then — HTTP 200
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        // And — DB has the new config
        ApplicationConfig saved = configRepository
                .findByApplicationAndEnvironment(appSlug, envSlug)
                .orElseThrow(() -> new AssertionError("Config not found in DB"));
        assertThat(saved.getSamplingRate()).isEqualTo(0.1);

        // And — CONFIG_UPDATE was pushed (addGroupCommandWithReplies called once)
        verify(registryService)
                .addGroupCommandWithReplies(eq(appSlug), eq(envSlug), eq(CommandType.CONFIG_UPDATE), any());
    }

    @Test
    void putConfig_liveExplicit_savesAndPushes() {
        // Same as above but with explicit apply=live
        String agentId = "live-explicit-" + UUID.randomUUID().toString().substring(0, 8);
        registerLiveAgent(agentId);

        ResponseEntity<String> response = putConfig("live");

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        verify(registryService)
                .addGroupCommandWithReplies(eq(appSlug), eq(envSlug), eq(CommandType.CONFIG_UPDATE), any());
    }

    @Test
    void putConfig_unknownApplyValue_returns400() {
        ResponseEntity<String> response = putConfig("BOGUS");
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);

        int auditCount = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM audit_log WHERE target = ?", Integer.class, appSlug);
        assertThat(auditCount).isZero();
    }

    @Test
    void putConfig_staged_auditActionIsStagedAppConfig() {
        registerLiveAgent("audit-agent-" + UUID.randomUUID().toString().substring(0, 8));

        ResponseEntity<String> response = putConfig("staged");

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        List<String> actions = jdbcTemplate.queryForList(
                "SELECT action FROM audit_log WHERE target = ? ORDER BY timestamp DESC",
                String.class, appSlug);
        assertThat(actions).hasSize(1);
        assertThat(actions.get(0)).isEqualTo("stage_app_config");
    }

    @Test
    void putConfig_live_auditActionIsUpdateAppConfig() {
        registerLiveAgent("audit-agent-live-" + UUID.randomUUID().toString().substring(0, 8));

        ResponseEntity<String> response = putConfig(null);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        List<String> actions = jdbcTemplate.queryForList(
                "SELECT action FROM audit_log WHERE target = ? ORDER BY timestamp DESC",
                String.class, appSlug);
        assertThat(actions).hasSize(1);
        assertThat(actions.get(0)).isEqualTo("update_app_config");
    }
}
