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

import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.search.ClickHouseLogStore;
import com.cameleer.server.core.alerting.*;
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.boot.test.mock.mockito.MockBean;

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

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

class PostgresAlertNotificationRepositoryIT extends AbstractPostgresIT {

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

    private PostgresAlertNotificationRepository repo;
    private UUID envId;
    private UUID instanceId;

    @BeforeEach
    void setup() {
        repo = new PostgresAlertNotificationRepository(jdbcTemplate, new ObjectMapper());
        envId = UUID.randomUUID();
        instanceId = UUID.randomUUID();
        UUID ruleId = UUID.randomUUID();

        jdbcTemplate.update(
            "INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?)",
            envId, "test-env-" + UUID.randomUUID(), "Test Env");
        jdbcTemplate.update(
            "INSERT INTO users (user_id, provider, email) VALUES ('sys-user', 'local', 'sys@example.com') ON CONFLICT (user_id) DO NOTHING");
        jdbcTemplate.update(
            "INSERT INTO alert_rules (id, environment_id, name, severity, condition_kind, condition, " +
            "notification_title_tmpl, notification_message_tmpl, created_by, updated_by) " +
            "VALUES (?, ?, 'rule', 'WARNING', 'AGENT_STATE', '{}'::jsonb, 't', 'm', 'sys-user', 'sys-user')",
            ruleId, envId);
        jdbcTemplate.update(
            "INSERT INTO alert_instances (id, rule_id, rule_snapshot, environment_id, state, severity, " +
            "fired_at, context, title, message) VALUES (?, ?, '{}'::jsonb, ?, 'FIRING', 'WARNING', " +
            "now(), '{}'::jsonb, 'title', 'msg')",
            instanceId, ruleId, envId);
    }

    @AfterEach
    void cleanup() {
        jdbcTemplate.update("DELETE FROM alert_notifications WHERE alert_instance_id = ?", instanceId);
        jdbcTemplate.update("DELETE FROM alert_instances WHERE environment_id = ?", envId);
        jdbcTemplate.update("DELETE FROM alert_rules WHERE environment_id = ?", envId);
        jdbcTemplate.update("DELETE FROM environments WHERE id = ?", envId);
    }

    @Test
    void saveAndFindByIdRoundtrip() {
        var n = newNotification();
        repo.save(n);

        var found = repo.findById(n.id()).orElseThrow();
        assertThat(found.id()).isEqualTo(n.id());
        assertThat(found.status()).isEqualTo(NotificationStatus.PENDING);
        assertThat(found.alertInstanceId()).isEqualTo(instanceId);
        assertThat(found.payload()).containsKey("key");
    }

    @Test
    void claimDueNotifications_claimsAndSkipsLocked() {
        var n = newNotification();
        repo.save(n);

        var claimed = repo.claimDueNotifications("worker-1", 10, 30);
        assertThat(claimed).hasSize(1);
        assertThat(claimed.get(0).claimedBy()).isEqualTo("worker-1");

        // second claimant sees nothing
        var second = repo.claimDueNotifications("worker-2", 10, 30);
        assertThat(second).isEmpty();
    }

    @Test
    void markDelivered_setsStatusAndDeliveredAt() {
        var n = newNotification();
        repo.save(n);

        repo.markDelivered(n.id(), 200, "OK", Instant.now());

        var found = repo.findById(n.id()).orElseThrow();
        assertThat(found.status()).isEqualTo(NotificationStatus.DELIVERED);
        assertThat(found.lastResponseStatus()).isEqualTo(200);
        assertThat(found.deliveredAt()).isNotNull();
    }

    @Test
    void scheduleRetry_bumpsAttemptsAndNextAttempt() {
        var n = newNotification();
        repo.save(n);

        Instant nextAttempt = Instant.now().plusSeconds(60);
        repo.scheduleRetry(n.id(), nextAttempt, 503, "Service Unavailable");

        var found = repo.findById(n.id()).orElseThrow();
        assertThat(found.attempts()).isEqualTo(1);
        assertThat(found.status()).isEqualTo(NotificationStatus.PENDING); // still pending
        assertThat(found.lastResponseStatus()).isEqualTo(503);
    }

    @Test
    void markFailed_setsStatusFailed() {
        var n = newNotification();
        repo.save(n);

        repo.markFailed(n.id(), 400, "Bad Request");

        var found = repo.findById(n.id()).orElseThrow();
        assertThat(found.status()).isEqualTo(NotificationStatus.FAILED);
        assertThat(found.lastResponseStatus()).isEqualTo(400);
    }

    @Test
    void deleteSettledBefore_deletesDeliveredAndFailed() {
        var pending  = newNotification();
        var delivered = newNotification();
        var failed    = newNotification();

        repo.save(pending);
        repo.save(delivered);
        repo.save(failed);

        repo.markDelivered(delivered.id(), 200, "OK", Instant.now().minusSeconds(3600));
        repo.markFailed(failed.id(), 500, "Error");

        // deleteSettledBefore uses created_at — use future cutoff to delete all settled
        repo.deleteSettledBefore(Instant.now().plusSeconds(60));

        assertThat(repo.findById(pending.id())).isPresent();
        assertThat(repo.findById(delivered.id())).isEmpty();
        assertThat(repo.findById(failed.id())).isEmpty();
    }

    @Test
    void listForInstance_returnsAll() {
        repo.save(newNotification());
        repo.save(newNotification());

        var list = repo.listForInstance(instanceId);
        assertThat(list).hasSize(2);
    }

    // -------------------------------------------------------------------------

    private AlertNotification newNotification() {
        return new AlertNotification(
            UUID.randomUUID(), instanceId,
            UUID.randomUUID(), null,
            NotificationStatus.PENDING, 0,
            Instant.now().minusSeconds(10),
            null, null,
            null, null,
            Map.of("key", "value"),
            null, Instant.now());
    }
}
