package io.cameleer.server.app.license;

import io.cameleer.server.app.AbstractPostgresIT;
import io.cameleer.server.app.TestSecurityHelper;
import io.cameleer.server.app.search.ClickHouseLogStore;
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.Nested;
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.util.Map;

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

/**
 * Cross-cutting smoke regression for the license cap framework (spec §10).
 *
 * <p>This IT installs a SINGLE synthetic license with all caps lowered to the smallest useful
 * value (1 each, except {@code max_users = 2} so the seeded admin write doesn't immediately
 * exhaust the cap) and verifies that five different cap surfaces fire under the SAME license.
 * The point is not to exhaustively test each cap — per-limit ITs already do that — but to catch
 * the regression where one of the wiring tasks is accidentally backed out without a per-limit
 * test failing. If the consolidated tripwire here fires, we know the framework is uniformly
 * wired across all surfaces.</p>
 *
 * <p>Each {@link Nested} test:</p>
 * <ol>
 *   <li>Pushes the endpoint up to the cap (the outer {@link BeforeEach} pre-cleans state).</li>
 *   <li>Pushes once more — expects 403 with the standard envelope produced by
 *   {@link LicenseExceptionAdvice}: {@code error="license cap reached"}, {@code limit},
 *   {@code current}, {@code cap}, {@code state="ACTIVE"}, non-blank {@code message}.</li>
 *   <li>Verifies {@code audit_log} has at least one row with {@code category='LICENSE'},
 *   {@code action='cap_exceeded'}, {@code result='FAILURE'}, {@code target=<key>}.</li>
 * </ol>
 *
 * <p>Out of scope (already covered by per-limit ITs):</p>
 * <ul>
 *   <li>Agent registration cap — see {@code AgentCapEnforcementIT}.</li>
 *   <li>Compute caps (cpu/memory/replicas) — see {@code ComputeCapEnforcementIT}; the deploy
 *   endpoint requires a real artifact and runtime orchestration.</li>
 *   <li>JAR retention cap — see {@code RetentionCapEnforcementIT}; that is a 422 not a 403,
 *   shaped differently from the cap envelope.</li>
 * </ul>
 */
class LicenseEnforcementIT extends AbstractPostgresIT {

    // The alert-rule controller wires evaluators that touch the CH log store bean. Mocking it
    // mirrors {@code AlertRuleCapEnforcementIT}'s pattern and avoids requiring real CH log
    // behaviour for a smoke regression that never evaluates rules.
    @MockBean(name = "clickHouseLogStore") ClickHouseLogStore clickHouseLogStore;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private TestSecurityHelper securityHelper;

    private String adminJwt;

    @BeforeEach
    void installLicense() {
        adminJwt = securityHelper.adminToken();
        // Defensive: clear any license a previous IT may have left installed (LicenseGate is a
        // singleton across @SpringBootTest context reuse).
        securityHelper.clearTestLicense();
        // Strip dependents in FK order before the parent rows.
        jdbcTemplate.update("DELETE FROM alert_notifications");
        jdbcTemplate.update("DELETE FROM alert_instances");
        jdbcTemplate.update("DELETE FROM alert_silences");
        jdbcTemplate.update("DELETE FROM alert_rule_targets");
        jdbcTemplate.update("DELETE FROM alert_rules");
        jdbcTemplate.update("DELETE FROM deployments");
        jdbcTemplate.update("DELETE FROM app_versions");
        jdbcTemplate.update("DELETE FROM apps");
        jdbcTemplate.update("DELETE FROM environments WHERE slug != 'default'");
        jdbcTemplate.update("DELETE FROM outbound_connections");
        jdbcTemplate.update("DELETE FROM user_roles");
        jdbcTemplate.update("DELETE FROM user_groups");
        jdbcTemplate.update("DELETE FROM users");
        jdbcTemplate.update("DELETE FROM audit_log");
        // Seed user row for the JWT subject — alert_rules.created_by and
        // outbound_connections.created_by FK to users(user_id).
        jdbcTemplate.update(
                "INSERT INTO users (user_id, provider, email, display_name) VALUES (?, 'test', ?, ?) ON CONFLICT (user_id) DO NOTHING",
                "test-admin", "test-admin@example.com", "test-admin");
        // Single synthetic license used by all @Nested tests. Caps set to the minimum useful
        // values so the cap rejection lands on a small number of HTTP calls.
        // NB: max_users = 2 because the seeded test-admin row already counts toward the cap;
        // creating one more user succeeds, the second additional create rejects.
        securityHelper.installSyntheticUnsignedLicense(Map.of(
                "max_environments", 1,
                "max_apps", 1,
                "max_outbound_connections", 1,
                "max_alert_rules", 1,
                "max_users", 2));
    }

    @AfterEach
    void clearLicense() {
        securityHelper.clearTestLicense();
    }

    // ---------- shared helpers ----------

    private void assert403CapEnvelope(ResponseEntity<String> response, String expectedLimit, int expectedCap) throws Exception {
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
        JsonNode body = objectMapper.readTree(response.getBody());
        assertThat(body.path("error").asText()).isEqualTo("license cap reached");
        assertThat(body.path("limit").asText()).isEqualTo(expectedLimit);
        assertThat(body.path("cap").asInt()).isEqualTo(expectedCap);
        assertThat(body.path("state").asText()).isEqualTo("ACTIVE");
        assertThat(body.has("message")).isTrue();
        assertThat(body.path("message").asText()).isNotBlank();
    }

    private long auditCount(String target) {
        Long count = jdbcTemplate.queryForObject("""
                SELECT COUNT(*) FROM audit_log
                WHERE category = 'LICENSE'
                  AND action = 'cap_exceeded'
                  AND result = 'FAILURE'
                  AND target = ?
                """, Long.class, target);
        return count == null ? 0L : count;
    }

    // ---------- nested cap tests ----------

    @Nested
    class EnvironmentCap {
        @Test
        void secondCreate_rejectedWith403AndAuditRow() throws Exception {
            // V1 seeds the "default" env, so even a single create exceeds max_environments=1.
            String json = """
                    {"slug":"prod","displayName":"Prod","production":true}
                    """;
            ResponseEntity<String> response = restTemplate.exchange(
                    "/api/v1/admin/environments", HttpMethod.POST,
                    new HttpEntity<>(json, securityHelper.authHeaders(adminJwt)),
                    String.class);

            assert403CapEnvelope(response, "max_environments", 1);
            assertThat(auditCount("max_environments")).isGreaterThanOrEqualTo(1);
        }
    }

    @Nested
    class AppCap {
        @Test
        void secondCreate_rejectedWith403AndAuditRow() throws Exception {
            // First create succeeds (count: 0 -> 1, cap=1).
            ResponseEntity<String> first = restTemplate.exchange(
                    "/api/v1/environments/default/apps", HttpMethod.POST,
                    new HttpEntity<>("""
                            {"slug":"a1","displayName":"A1"}
                            """, securityHelper.authHeaders(adminJwt)),
                    String.class);
            assertThat(first.getStatusCode()).isEqualTo(HttpStatus.CREATED);

            // Second create rejects.
            ResponseEntity<String> second = restTemplate.exchange(
                    "/api/v1/environments/default/apps", HttpMethod.POST,
                    new HttpEntity<>("""
                            {"slug":"a2","displayName":"A2"}
                            """, securityHelper.authHeaders(adminJwt)),
                    String.class);

            assert403CapEnvelope(second, "max_apps", 1);
            assertThat(auditCount("max_apps")).isGreaterThanOrEqualTo(1);
        }
    }

    @Nested
    class OutboundConnectionCap {
        @Test
        void secondCreate_rejectedWith403AndAuditRow() throws Exception {
            ResponseEntity<String> first = restTemplate.exchange(
                    "/api/v1/admin/outbound-connections", HttpMethod.POST,
                    new HttpEntity<>("""
                            {"name":"hook-1","url":"https://hooks.example.com/1","method":"POST",
                             "tlsTrustMode":"SYSTEM_DEFAULT","auth":{}}""",
                            securityHelper.authHeaders(adminJwt)),
                    String.class);
            assertThat(first.getStatusCode()).isEqualTo(HttpStatus.CREATED);

            ResponseEntity<String> second = restTemplate.exchange(
                    "/api/v1/admin/outbound-connections", HttpMethod.POST,
                    new HttpEntity<>("""
                            {"name":"hook-2","url":"https://hooks.example.com/2","method":"POST",
                             "tlsTrustMode":"SYSTEM_DEFAULT","auth":{}}""",
                            securityHelper.authHeaders(adminJwt)),
                    String.class);

            assert403CapEnvelope(second, "max_outbound_connections", 1);
            assertThat(auditCount("max_outbound_connections")).isGreaterThanOrEqualTo(1);
        }
    }

    @Nested
    class AlertRuleCap {
        @Test
        void secondCreate_rejectedWith403AndAuditRow() throws Exception {
            ResponseEntity<String> first = restTemplate.exchange(
                    "/api/v1/environments/default/alerts/rules", HttpMethod.POST,
                    new HttpEntity<>(ruleBody("rule-1"), securityHelper.authHeaders(adminJwt)),
                    String.class);
            assertThat(first.getStatusCode()).isEqualTo(HttpStatus.CREATED);

            ResponseEntity<String> second = restTemplate.exchange(
                    "/api/v1/environments/default/alerts/rules", HttpMethod.POST,
                    new HttpEntity<>(ruleBody("rule-2"), securityHelper.authHeaders(adminJwt)),
                    String.class);

            assert403CapEnvelope(second, "max_alert_rules", 1);
            assertThat(auditCount("max_alert_rules")).isGreaterThanOrEqualTo(1);
        }

        private String ruleBody(String name) {
            // Mirrors AlertRuleCapEnforcementIT — minimal valid body that passes the
            // controller's "at least one webhook or target" guard.
            return """
                    {"name":"%s","severity":"WARNING","conditionKind":"ROUTE_METRIC",
                     "condition":{"kind":"ROUTE_METRIC","scope":{},
                       "metric":"ERROR_RATE","comparator":"GT","threshold":0.05,"windowSeconds":60},
                     "targets":[{"kind":"USER","targetId":"test-admin"}]}
                    """.formatted(name);
        }
    }

    @Nested
    class UserCap {
        @Test
        void thirdCreate_rejectedWith403AndAuditRow() throws Exception {
            // The outer @BeforeEach truncates users and seeds only "test-admin" (count = 1).
            // max_users = 2, so the FIRST create succeeds (count: 1 -> 2) and the SECOND rejects.
            ResponseEntity<String> first = createUser("alice");
            assertThat(first.getStatusCode()).isEqualTo(HttpStatus.OK);

            ResponseEntity<String> second = createUser("bob");
            assert403CapEnvelope(second, "max_users", 2);
            assertThat(auditCount("max_users")).isGreaterThanOrEqualTo(1);
        }

        private ResponseEntity<String> createUser(String username) {
            // Password meets the policy: 12+ chars, 3-of-4 char classes, doesn't match username.
            String body = """
                    {
                      "username": "%s",
                      "displayName": "User %s",
                      "email": "%s@example.com",
                      "password": "Sup3rSecret-Pass!"
                    }
                    """.formatted(username, username, username);
            return restTemplate.exchange(
                    "/api/v1/admin/users", HttpMethod.POST,
                    new HttpEntity<>(body, securityHelper.authHeaders(adminJwt)),
                    String.class);
        }
    }
}
