package io.cameleer.server.app.license;

import io.cameleer.server.app.AbstractPostgresIT;
import io.cameleer.server.app.TestSecurityHelper;
import io.cameleer.server.core.license.LicenseGate;
import io.cameleer.license.LicenseState;
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.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.Map;

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

/**
 * Integration test for Spec §4.3 — when a license changes, {@link RetentionPolicyApplier}
 * re-issues {@code ALTER TABLE … MODIFY TTL … WHERE environment = '<slug>'} statements
 * against ClickHouse so per-env retention reflects the new {@code min(licenseCap, env.configured)}.
 *
 * <p><b>Strategy:</b> the applier is an {@code @Async @EventListener}. To avoid timing
 * flakiness, this IT calls {@link RetentionPolicyApplier#onLicenseChanged} synchronously
 * after seeding the {@link LicenseGate} via
 * {@link TestSecurityHelper#installSyntheticUnsignedLicense(Map)}. The async dispatch
 * and the SQL-shape correctness are already covered by {@code RetentionPolicyApplierTest}
 * (unit, mocked CH); this IT adds the missing real-CH ALTER round-trip.</p>
 *
 * <p><b>Env retention setup:</b> V1 seeds the default env with retention=1 day. To make
 * the license cap the binding constraint of {@code min(cap, configured)}, this test
 * raises the {@code default} env's {@code log_retention_days} to 60 in {@code @BeforeEach}
 * via raw JDBC (the {@code EnvironmentRepository} interface deliberately does not expose
 * retention-day setters — admin endpoints exist for them, but the controller plumbing is
 * out of scope here). Restored to 1 in {@code @AfterEach}.</p>
 *
 * <p><b>Assertion target:</b> {@code system.tables.create_table_query} in ClickHouse
 * reflects the most recent {@code MODIFY TTL} clause as a row-level TTL with WHERE
 * predicate (CH 22.3+; project runs 24.12). Polls up to 5s for any other ALTERs that
 * the runtime may have queued.</p>
 */
class RetentionRuntimeRecomputeIT extends AbstractPostgresIT {

    @Autowired
    @Qualifier("clickHouseJdbcTemplate")
    JdbcTemplate clickHouseJdbc;

    @Autowired
    TestSecurityHelper securityHelper;

    @Autowired
    LicenseGate gate;

    @Autowired
    RetentionPolicyApplier applier;

    @BeforeEach
    void seedHighEnvRetention() {
        // Sibling ITs may have left a license in the gate — clear so default tier is the baseline.
        securityHelper.clearTestLicense();
        // Raise default env's log retention to 60 so license cap becomes the binding constraint.
        jdbcTemplate.update(
                "UPDATE environments SET log_retention_days = 60 WHERE slug = 'default'");
    }

    @AfterEach
    void restoreDefaults() {
        securityHelper.clearTestLicense();
        jdbcTemplate.update(
                "UPDATE environments SET log_retention_days = 1 WHERE slug = 'default'");
        // Re-fire applier with cleared gate (default tier, log cap = 1) so subsequent ITs
        // observe the schema's seeded TTL, not whatever this test last set.
        applier.onLicenseChanged(new LicenseChangedEvent(gate.getState(), gate.getCurrent()));
    }

    @Test
    void changingLicenseRecomputesLogsTtl() throws Exception {
        // (1) Install license with cap = 30. With env.configured = 60, effective = min(30, 60) = 30.
        securityHelper.installSyntheticUnsignedLicense(Map.of("max_log_retention_days", 30));
        assertThat(gate.getState()).isEqualTo(LicenseState.ACTIVE);
        assertThat(gate.getEffectiveLimits().get("max_log_retention_days")).isEqualTo(30);

        applier.onLicenseChanged(new LicenseChangedEvent(gate.getState(), gate.getCurrent()));

        awaitTtlInterval("logs", 30, 5_000);

        // (2) Replace license: cap = 7. effective = min(7, 60) = 7.
        securityHelper.clearTestLicense();
        securityHelper.installSyntheticUnsignedLicense(Map.of("max_log_retention_days", 7));
        assertThat(gate.getEffectiveLimits().get("max_log_retention_days")).isEqualTo(7);

        applier.onLicenseChanged(new LicenseChangedEvent(gate.getState(), gate.getCurrent()));

        awaitTtlInterval("logs", 7, 5_000);
    }

    /**
     * Polls {@code system.tables.create_table_query} until it contains the expected
     * day-interval fragment for the given environment, or the deadline elapses. The
     * synchronous applier call already completed before this is invoked — the poll
     * just guards against any latent ClickHouse internal propagation delay between
     * the ALTER returning and {@code system.tables} reflecting the change.
     *
     * <p>ClickHouse normalises {@code INTERVAL N DAY} in the stored {@code TTL}
     * clause to {@code toIntervalDay(N)} when serialising back to {@code create_table_query},
     * so we match the canonical form. The {@code WHERE environment = '<slug>'}
     * predicate is included so a stale TTL for a different env can't satisfy the
     * assertion.</p>
     */
    private void awaitTtlInterval(String table, int days, long timeoutMs) throws InterruptedException {
        long deadline = System.currentTimeMillis() + timeoutMs;
        String fragment = "toIntervalDay(" + days + ") WHERE environment = 'default'";
        String last = null;
        while (System.currentTimeMillis() < deadline) {
            last = clickHouseJdbc.queryForObject(
                    "SELECT create_table_query FROM system.tables "
                            + "WHERE name = ? AND database = currentDatabase()",
                    String.class, table);
            if (last != null && last.contains(fragment)) {
                return;
            }
            Thread.sleep(100);
        }
        throw new AssertionError(
                "Timed out waiting for " + table + " TTL to contain '" + fragment
                        + "'. Last create_table_query:\n" + last);
    }
}
