package io.cameleer.server.app.license;

import io.cameleer.server.core.license.LicenseGate;
import io.cameleer.license.LicenseInfo;
import io.cameleer.license.LicenseState;
import io.cameleer.server.core.runtime.Environment;
import io.cameleer.server.core.runtime.EnvironmentRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.jdbc.core.JdbcTemplate;

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class RetentionPolicyApplierTest {

    EnvironmentRepository envRepo;
    JdbcTemplate ch;
    LicenseGate gate;
    RetentionPolicyApplier applier;

    @BeforeEach
    void setUp() {
        envRepo = mock(EnvironmentRepository.class);
        ch = mock(JdbcTemplate.class);
        gate = new LicenseGate();
        applier = new RetentionPolicyApplier(gate, envRepo, ch);
    }

    private static Environment env(String slug, int execDays, int logDays, int metricDays) {
        return new Environment(
                UUID.randomUUID(), slug, slug, false, true,
                Map.of(), null, "slate", Instant.now(),
                execDays, logDays, metricDays);
    }

    private void licenseWithCaps(int execCap, int logCap, int metricCap) {
        LicenseInfo info = new LicenseInfo(
                UUID.randomUUID(), "default", null,
                Map.of(
                        "max_execution_retention_days", execCap,
                        "max_log_retention_days", logCap,
                        "max_metric_retention_days", metricCap
                ),
                Instant.now(),
                Instant.now().plusSeconds(86400),
                0);
        gate.load(info);
    }

    private static LicenseChangedEvent ev() {
        return new LicenseChangedEvent(LicenseState.ACTIVE, null);
    }

    @Test
    void onChange_emitsAlterPerTablePerEnv() {
        licenseWithCaps(30, 30, 30);
        Environment dev = env("dev", 30, 30, 30);
        when(envRepo.findAll()).thenReturn(List.of(dev));

        applier.onLicenseChanged(ev());

        // 5 retention-bound tables defined in SPECS
        ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
        verify(ch, times(RetentionPolicyApplier.SPECS.size())).execute(sql.capture());

        List<String> all = sql.getAllValues();
        // every statement is an ALTER TABLE … MODIFY TTL … WHERE environment = 'dev'
        assertThat(all).allSatisfy(s -> {
            assertThat(s).startsWith("ALTER TABLE ");
            assertThat(s).contains("MODIFY TTL toDateTime(");
            assertThat(s).contains(" DAY DELETE");
            assertThat(s).endsWith(" WHERE environment = 'dev'");
        });

        // Sanity-check the per-table time column wiring
        assertThat(findFor(all, "executions"))
                .contains("toDateTime(start_time)");
        assertThat(findFor(all, "processor_executions"))
                .contains("toDateTime(start_time)");
        assertThat(findFor(all, "logs"))
                .contains("toDateTime(timestamp)");
        assertThat(findFor(all, "agent_metrics"))
                .contains("toDateTime(collected_at)");
        assertThat(findFor(all, "agent_events"))
                .contains("toDateTime(timestamp)");
    }

    @Test
    void chFailure_doesNotPropagate() {
        licenseWithCaps(30, 30, 30);
        when(envRepo.findAll()).thenReturn(List.of(env("dev", 30, 30, 30)));
        doThrow(new RuntimeException("ch down")).when(ch).execute(anyString());

        assertThatCode(() -> applier.onLicenseChanged(ev()))
                .doesNotThrowAnyException();

        // listener still attempted every (env, table) pair despite failures
        verify(ch, times(RetentionPolicyApplier.SPECS.size())).execute(anyString());
    }

    @Test
    void multipleEnvs_emitsPerEnvAlter() {
        licenseWithCaps(30, 30, 30);
        when(envRepo.findAll()).thenReturn(List.of(
                env("dev", 30, 30, 30),
                env("prod", 30, 30, 30)
        ));

        applier.onLicenseChanged(ev());

        ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
        int expected = RetentionPolicyApplier.SPECS.size() * 2;
        verify(ch, times(expected)).execute(sql.capture());

        long devCount = sql.getAllValues().stream()
                .filter(s -> s.endsWith("WHERE environment = 'dev'")).count();
        long prodCount = sql.getAllValues().stream()
                .filter(s -> s.endsWith("WHERE environment = 'prod'")).count();
        assertThat(devCount).isEqualTo(RetentionPolicyApplier.SPECS.size());
        assertThat(prodCount).isEqualTo(RetentionPolicyApplier.SPECS.size());
    }

    @Test
    void effectiveDaysIsMinOfCapAndConfigured_capWins() {
        // env wants 30 days but license caps at 7 → expect INTERVAL 7 DAY
        licenseWithCaps(7, 7, 7);
        when(envRepo.findAll()).thenReturn(List.of(env("dev", 30, 30, 30)));

        applier.onLicenseChanged(ev());

        ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
        verify(ch, times(RetentionPolicyApplier.SPECS.size())).execute(sql.capture());
        assertThat(sql.getAllValues())
                .allSatisfy(s -> assertThat(s).contains("INTERVAL 7 DAY DELETE"));
    }

    @Test
    void effectiveDaysIsMinOfCapAndConfigured_configuredWins() {
        // env wants 3 days, license allows up to 7 → expect INTERVAL 3 DAY
        licenseWithCaps(7, 7, 7);
        when(envRepo.findAll()).thenReturn(List.of(env("dev", 3, 3, 3)));

        applier.onLicenseChanged(ev());

        ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
        verify(ch, times(RetentionPolicyApplier.SPECS.size())).execute(sql.capture());
        assertThat(sql.getAllValues())
                .allSatisfy(s -> assertThat(s).contains("INTERVAL 3 DAY DELETE"));
    }

    @Test
    void mixedCapAndConfigured_perTable() {
        // distinct caps per axis; env exec=10, log=2, metric=50
        // exec  : min(20, 10) = 10
        // log   : min(20, 2)  = 2
        // metric: min(20, 50) = 20
        licenseWithCaps(20, 20, 20);
        when(envRepo.findAll()).thenReturn(List.of(env("dev", 10, 2, 50)));

        applier.onLicenseChanged(ev());

        ArgumentCaptor<String> sql = ArgumentCaptor.forClass(String.class);
        verify(ch, times(RetentionPolicyApplier.SPECS.size())).execute(sql.capture());
        List<String> all = sql.getAllValues();

        assertThat(findFor(all, "executions")).contains("INTERVAL 10 DAY");
        assertThat(findFor(all, "processor_executions")).contains("INTERVAL 10 DAY");
        assertThat(findFor(all, "logs")).contains("INTERVAL 2 DAY");
        assertThat(findFor(all, "agent_metrics")).contains("INTERVAL 20 DAY");
        assertThat(findFor(all, "agent_events")).contains("INTERVAL 20 DAY");
    }

    /** Pick the single SQL statement that targets the given table. */
    private static String findFor(List<String> all, String table) {
        String prefix = "ALTER TABLE " + table + " ";
        return all.stream()
                .filter(s -> s.startsWith(prefix))
                .findFirst()
                .orElseThrow(() -> new AssertionError("no SQL for table " + table + " in: " + all));
    }
}
