package com.cameleer.server.app.alerting.controller;

import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper;
import com.cameleer.server.app.search.ClickHouseLogStore;
import com.cameleer.server.core.alerting.AlertInstance;
import com.cameleer.server.core.alerting.AlertInstanceRepository;
import com.cameleer.server.core.alerting.AlertSeverity;
import com.cameleer.server.core.alerting.AlertState;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.MockBean;
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 java.time.Instant;
import java.util.List;
import java.util.UUID;

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

class AlertControllerIT extends AbstractPostgresIT {

    @MockBean(name = "clickHouseLogStore")    ClickHouseLogStore    clickHouseLogStore;

    @Autowired private TestRestTemplate restTemplate;
    @Autowired private ObjectMapper objectMapper;
    @Autowired private TestSecurityHelper securityHelper;
    @Autowired private AlertInstanceRepository instanceRepo;

    private String operatorJwt;
    private String viewerJwt;
    private String envSlugA;
    private String envSlugB;
    private UUID envIdA;
    private UUID envIdB;

    @BeforeEach
    void setUp() {
        operatorJwt = securityHelper.operatorToken();
        viewerJwt   = securityHelper.viewerToken();
        seedUser("test-operator");
        seedUser("test-viewer");

        envSlugA = "alert-env-a-" + UUID.randomUUID().toString().substring(0, 6);
        envSlugB = "alert-env-b-" + UUID.randomUUID().toString().substring(0, 6);
        envIdA = UUID.randomUUID();
        envIdB = UUID.randomUUID();
        jdbcTemplate.update(
                "INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?) ON CONFLICT (id) DO NOTHING",
                envIdA, envSlugA, envSlugA);
        jdbcTemplate.update(
                "INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?) ON CONFLICT (id) DO NOTHING",
                envIdB, envSlugB, envSlugB);
    }

    @AfterEach
    void cleanUp() {
        jdbcTemplate.update("DELETE FROM alert_notifications WHERE alert_instance_id IN (SELECT id FROM alert_instances WHERE environment_id IN (?, ?))", envIdA, envIdB);
        jdbcTemplate.update("DELETE FROM alert_instances WHERE environment_id IN (?, ?)", envIdA, envIdB);
        jdbcTemplate.update("DELETE FROM environments WHERE id IN (?, ?)", envIdA, envIdB);
        jdbcTemplate.update("DELETE FROM users WHERE user_id IN ('test-operator','test-viewer')");
    }

    // -------------------------------------------------------------------------
    // Existing tests (baseline)
    // -------------------------------------------------------------------------

    @Test
    void listReturnsAlertsForEnv() throws Exception {
        AlertInstance instance = seedInstance(envIdA);

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode body = objectMapper.readTree(resp.getBody());
        assertThat(body.isArray()).isTrue();
        boolean found = false;
        for (JsonNode node : body) {
            if (node.path("id").asText().equals(instance.id().toString())) {
                found = true;
                break;
            }
        }
        assertThat(found).as("seeded alert must appear in env-A inbox").isTrue();
    }

    @Test
    void envIsolation() throws Exception {
        AlertInstance instanceA = seedInstance(envIdA);

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugB + "/alerts",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);

        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode body = objectMapper.readTree(resp.getBody());
        for (JsonNode node : body) {
            assertThat(node.path("id").asText())
                    .as("env-A alert must not appear in env-B inbox")
                    .isNotEqualTo(instanceA.id().toString());
        }
    }

    @Test
    void unreadCountReturnsNumber() {
        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/unread-count",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
    }

    @Test
    void ackFlow() throws Exception {
        AlertInstance instance = seedInstance(envIdA);

        ResponseEntity<String> ack = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id() + "/ack",
                HttpMethod.POST,
                new HttpEntity<>(securityHelper.authHeaders(operatorJwt)),
                String.class);

        assertThat(ack.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode body = objectMapper.readTree(ack.getBody());
        assertThat(body.path("state").asText()).isEqualTo("FIRING");
    }

    @Test
    void readMarksSingleAlert() throws Exception {
        AlertInstance instance = seedInstance(envIdA);

        ResponseEntity<String> read = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id() + "/read",
                HttpMethod.POST,
                new HttpEntity<>(securityHelper.authHeaders(operatorJwt)),
                String.class);

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

        // Verify persistence — readAt must now be set
        AlertInstance updated = instanceRepo.findById(instance.id()).orElseThrow();
        assertThat(updated.readAt()).as("readAt must be set after /read").isNotNull();
    }

    @Test
    void bulkRead() throws Exception {
        AlertInstance i1 = seedInstance(envIdA);
        AlertInstance i2 = seedInstance(envIdA);

        String body = """
                {"instanceIds":["%s","%s"]}
                """.formatted(i1.id(), i2.id());

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/bulk-read",
                HttpMethod.POST,
                new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
                String.class);

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

        // Verify persistence — both must have readAt set
        assertThat(instanceRepo.findById(i1.id()).orElseThrow().readAt())
                .as("i1 readAt must be set after bulk-read").isNotNull();
        assertThat(instanceRepo.findById(i2.id()).orElseThrow().readAt())
                .as("i2 readAt must be set after bulk-read").isNotNull();
    }

    @Test
    void viewerCanRead() {
        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)),
                String.class);
        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
    }

    // -------------------------------------------------------------------------
    // New endpoint tests
    // -------------------------------------------------------------------------

    @Test
    void delete_softDeletes_and_subsequent_get_returns_404() {
        AlertInstance instance = seedInstance(envIdA);

        // OPERATOR deletes
        ResponseEntity<String> del = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id(),
                HttpMethod.DELETE,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(del.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);

        // Subsequent GET returns 404
        ResponseEntity<String> get = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id(),
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(get.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }

    @Test
    void delete_non_operator_returns_403() {
        AlertInstance instance = seedInstance(envIdA);

        ResponseEntity<String> del = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id(),
                HttpMethod.DELETE,
                new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)),
                String.class);
        assertThat(del.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
    }

    @Test
    void bulkDelete_only_affects_matching_env() {
        AlertInstance inEnvA  = seedInstance(envIdA);
        AlertInstance inEnvA2 = seedInstance(envIdA);
        AlertInstance inEnvB  = seedInstance(envIdB);

        String body = """
                {"instanceIds":["%s","%s","%s"]}
                """.formatted(inEnvA.id(), inEnvA2.id(), inEnvB.id());

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/bulk-delete",
                HttpMethod.POST,
                new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
                String.class);
        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);

        // env-A alerts should be soft-deleted
        assertThat(instanceRepo.findById(inEnvA.id()).orElseThrow().deletedAt())
                .as("inEnvA should be soft-deleted").isNotNull();
        assertThat(instanceRepo.findById(inEnvA2.id()).orElseThrow().deletedAt())
                .as("inEnvA2 should be soft-deleted").isNotNull();

        // env-B alert must NOT be soft-deleted
        assertThat(instanceRepo.findById(inEnvB.id()).orElseThrow().deletedAt())
                .as("inEnvB must not be soft-deleted via env-A bulk-delete").isNull();
    }

    @Test
    void bulkAck_only_touches_unacked_rows() {
        AlertInstance i1 = seedInstance(envIdA);
        AlertInstance i2 = seedInstance(envIdA);

        // Pre-ack i1 with an existing user (must be in users table due to FK)
        instanceRepo.ack(i1.id(), "test-viewer", Instant.now().minusSeconds(60));

        String body = """
                {"instanceIds":["%s","%s"]}
                """.formatted(i1.id(), i2.id());

        ResponseEntity<String> resp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/bulk-ack",
                HttpMethod.POST,
                new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)),
                String.class);
        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);

        // i1's ackedBy must remain "test-viewer" (bulk-ack skips already-acked rows)
        AlertInstance refreshed1 = instanceRepo.findById(i1.id()).orElseThrow();
        assertThat(refreshed1.ackedBy()).as("previously-acked row must keep original ackedBy").isEqualTo("test-viewer");

        // i2 must now be acked
        AlertInstance refreshed2 = instanceRepo.findById(i2.id()).orElseThrow();
        assertThat(refreshed2.ackedAt()).as("i2 must be acked after bulk-ack").isNotNull();
    }

    @Test
    void restore_clears_deleted_at_and_reappears_in_inbox() throws Exception {
        AlertInstance instance = seedInstance(envIdA);

        // Soft-delete first
        restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id(),
                HttpMethod.DELETE,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(instanceRepo.findById(instance.id()).orElseThrow().deletedAt()).isNotNull();

        // Restore
        ResponseEntity<String> restoreResp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id() + "/restore",
                HttpMethod.POST,
                new HttpEntity<>(securityHelper.authHeaders(operatorJwt)),
                String.class);
        assertThat(restoreResp.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);

        // deletedAt must be cleared
        assertThat(instanceRepo.findById(instance.id()).orElseThrow().deletedAt())
                .as("deletedAt must be null after restore").isNull();

        // Alert reappears in inbox list
        ResponseEntity<String> listResp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(listResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode body = objectMapper.readTree(listResp.getBody());
        boolean found = false;
        for (JsonNode node : body) {
            if (node.path("id").asText().equals(instance.id().toString())) {
                found = true;
                break;
            }
        }
        assertThat(found).as("restored alert must reappear in inbox").isTrue();
    }

    @Test
    void list_respects_acked_filter_tristate() throws Exception {
        AlertInstance unacked = seedInstance(envIdA);
        AlertInstance acked   = seedInstance(envIdA);
        instanceRepo.ack(acked.id(), "test-operator", Instant.now());

        // ?acked=false — only unacked
        ResponseEntity<String> falseResp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts?acked=false",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(falseResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode falseBody = objectMapper.readTree(falseResp.getBody());
        boolean unackedFound = false, ackedFoundInFalse = false;
        for (JsonNode node : falseBody) {
            String id = node.path("id").asText();
            if (id.equals(unacked.id().toString())) unackedFound = true;
            if (id.equals(acked.id().toString()))   ackedFoundInFalse = true;
        }
        assertThat(unackedFound).as("unacked alert must appear with ?acked=false").isTrue();
        assertThat(ackedFoundInFalse).as("acked alert must NOT appear with ?acked=false").isFalse();

        // ?acked=true — only acked
        ResponseEntity<String> trueResp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts?acked=true",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(trueResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode trueBody = objectMapper.readTree(trueResp.getBody());
        boolean ackedFound = false, unackedFoundInTrue = false;
        for (JsonNode node : trueBody) {
            String id = node.path("id").asText();
            if (id.equals(acked.id().toString()))   ackedFound = true;
            if (id.equals(unacked.id().toString())) unackedFoundInTrue = true;
        }
        assertThat(ackedFound).as("acked alert must appear with ?acked=true").isTrue();
        assertThat(unackedFoundInTrue).as("unacked alert must NOT appear with ?acked=true").isFalse();

        // no param — both visible
        ResponseEntity<String> allResp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(allResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode allBody = objectMapper.readTree(allResp.getBody());
        boolean bothUnacked = false, bothAcked = false;
        for (JsonNode node : allBody) {
            String id = node.path("id").asText();
            if (id.equals(unacked.id().toString())) bothUnacked = true;
            if (id.equals(acked.id().toString()))   bothAcked = true;
        }
        assertThat(bothUnacked).as("unacked must appear with no acked filter").isTrue();
        assertThat(bothAcked).as("acked must appear with no acked filter").isTrue();
    }

    @Test
    void list_respects_read_filter_tristate() throws Exception {
        AlertInstance unread = seedInstance(envIdA);
        AlertInstance read   = seedInstance(envIdA);
        instanceRepo.markRead(read.id(), Instant.now());

        // ?read=false — only unread
        ResponseEntity<String> falseResp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts?read=false",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(falseResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode falseBody = objectMapper.readTree(falseResp.getBody());
        boolean unreadFound = false, readFoundInFalse = false;
        for (JsonNode node : falseBody) {
            String id = node.path("id").asText();
            if (id.equals(unread.id().toString())) unreadFound = true;
            if (id.equals(read.id().toString()))   readFoundInFalse = true;
        }
        assertThat(unreadFound).as("unread alert must appear with ?read=false").isTrue();
        assertThat(readFoundInFalse).as("read alert must NOT appear with ?read=false").isFalse();

        // ?read=true — only read
        ResponseEntity<String> trueResp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts?read=true",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(trueResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode trueBody = objectMapper.readTree(trueResp.getBody());
        boolean readFound = false, unreadFoundInTrue = false;
        for (JsonNode node : trueBody) {
            String id = node.path("id").asText();
            if (id.equals(read.id().toString()))   readFound = true;
            if (id.equals(unread.id().toString())) unreadFoundInTrue = true;
        }
        assertThat(readFound).as("read alert must appear with ?read=true").isTrue();
        assertThat(unreadFoundInTrue).as("unread alert must NOT appear with ?read=true").isFalse();

        // no param — both visible
        ResponseEntity<String> allResp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)),
                String.class);
        assertThat(allResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode allBody = objectMapper.readTree(allResp.getBody());
        boolean bothUnread = false, bothRead = false;
        for (JsonNode node : allBody) {
            String id = node.path("id").asText();
            if (id.equals(unread.id().toString())) bothUnread = true;
            if (id.equals(read.id().toString()))   bothRead = true;
        }
        assertThat(bothUnread).as("unread must appear with no read filter").isTrue();
        assertThat(bothRead).as("read must appear with no read filter").isTrue();
    }

    @Test
    void read_is_global_other_users_see_readAt_set() throws Exception {
        // Seed an alert targeting BOTH users so the viewer's GET /{id} is visible
        AlertInstance instance = new AlertInstance(
                UUID.randomUUID(), null, null, envIdA,
                AlertState.FIRING, AlertSeverity.WARNING,
                Instant.now(), null, null, null, null, null, null, false,
                42.0, 1000.0, null, "Global read test", "Operator reads, viewer sees it",
                List.of("test-operator", "test-viewer"), List.of(), List.of());
        instance = instanceRepo.save(instance);

        // Operator (user A) marks the alert as read
        ResponseEntity<String> readResp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id() + "/read",
                HttpMethod.POST,
                new HttpEntity<>(securityHelper.authHeaders(operatorJwt)),
                String.class);
        assertThat(readResp.getStatusCode()).isEqualTo(HttpStatus.OK);

        // Viewer (user B) fetches the same alert and must see readAt != null
        ResponseEntity<String> getResp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id(),
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)),
                String.class);
        assertThat(getResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode node = objectMapper.readTree(getResp.getBody());
        assertThat(node.path("readAt").isNull())
                .as("viewer must see readAt as non-null after operator marked read")
                .isFalse();
        assertThat(node.path("readAt").isMissingNode())
                .as("readAt field must be present in response")
                .isFalse();

        // Viewer's list endpoint must also show the alert with readAt set
        ResponseEntity<String> listResp = restTemplate.exchange(
                "/api/v1/environments/" + envSlugA + "/alerts",
                HttpMethod.GET,
                new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)),
                String.class);
        assertThat(listResp.getStatusCode()).isEqualTo(HttpStatus.OK);
        JsonNode listBody = objectMapper.readTree(listResp.getBody());
        UUID instanceId = instance.id();
        boolean foundWithReadAt = false;
        for (JsonNode item : listBody) {
            if (item.path("id").asText().equals(instanceId.toString())) {
                assertThat(item.path("readAt").isNull())
                        .as("list entry readAt must be non-null for viewer after global read")
                        .isFalse();
                foundWithReadAt = true;
                break;
            }
        }
        assertThat(foundWithReadAt).as("alert must appear in viewer's list with readAt set").isTrue();
    }

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

    private AlertInstance seedInstance(UUID envId) {
        // target by userId so the inbox SQL (? = ANY(target_user_ids)) matches the test-operator JWT
        // (JWT subject is "user:test-operator", stripped to "test-operator" by currentUserId())
        AlertInstance instance = new AlertInstance(
                UUID.randomUUID(), null, null, envId,
                AlertState.FIRING, AlertSeverity.WARNING,
                Instant.now(), null, null, null, null, null, null, false,
                42.0, 1000.0, null, "Test alert", "Something happened",
                List.of("test-operator"), List.of(), List.of());
        return instanceRepo.save(instance);
    }

    private void seedUser(String userId) {
        jdbcTemplate.update(
                "INSERT INTO users (user_id, provider, email, display_name) VALUES (?, 'test', ?, ?) ON CONFLICT (user_id) DO NOTHING",
                userId, userId + "@example.com", userId);
    }
}
