package com.cameleer.server.app.license;

import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper;
import com.cameleer.server.core.agent.AgentInfo;
import com.cameleer.server.core.agent.AgentRegistryService;
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.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import java.util.List;
import java.util.Map;

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

/**
 * Verifies that the {@code max_agents} cap is enforced at
 * {@code POST /api/v1/agents/register} via the {@link com.cameleer.server.core.runtime.CreateGuard}
 * wired into {@link AgentRegistryService}. The cap fires only on NEW registrations — re-registers
 * of an already-registered agent bypass the check (they don't grow the registry).
 *
 * <p>This IT installs a synthetic license that lowers {@code max_agents} to {@code 2} so the cap
 * can be exercised in a few HTTP calls. The default tier ({@code max_agents = 5}) is intentionally
 * not exercised here because sibling agent ITs share the Spring context's in-memory registry and
 * would interfere; the structured 403 envelope (also produced by
 * {@link LicenseExceptionAdvice}) is identical regardless of the underlying limit value.</p>
 */
class AgentCapEnforcementIT extends AbstractPostgresIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private TestSecurityHelper securityHelper;

    @Autowired
    private AgentRegistryService agentRegistryService;

    @BeforeEach
    void setUp() {
        // The registry is in-memory and shared across @SpringBootTest reuse boundaries; sibling
        // ITs (AgentRegistrationControllerIT, AgentSseControllerIT, …) leave residue here.
        clearRegistry();
        // Lower max_agents to 2 so the cap rejection lands on the third register call.
        securityHelper.installSyntheticUnsignedLicense(Map.of("max_agents", 2));
    }

    @AfterEach
    void tearDown() {
        securityHelper.clearTestLicense();
        clearRegistry();
    }

    private void clearRegistry() {
        List<AgentInfo> all = agentRegistryService.findAll();
        for (AgentInfo a : all) {
            agentRegistryService.deregister(a.instanceId());
        }
    }

    private ResponseEntity<String> register(String agentId) {
        String json = """
                {
                  "instanceId": "%s",
                  "applicationId": "test-cap",
                  "environmentId": "default",
                  "version": "1.0.0",
                  "routeIds": [],
                  "capabilities": {}
                }
                """.formatted(agentId);
        return restTemplate.postForEntity(
                "/api/v1/agents/register",
                new HttpEntity<>(json, securityHelper.bootstrapHeaders()),
                String.class);
    }

    @Test
    void registerBeyondCap_returns403WithStateAndMessage() throws Exception {
        // Two registrations succeed (cap = 2).
        assertThat(register("cap-it-agent-1").getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(register("cap-it-agent-2").getStatusCode()).isEqualTo(HttpStatus.OK);

        // The third registration must be rejected with the structured 403 envelope.
        ResponseEntity<String> third = register("cap-it-agent-3");
        assertThat(third.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);

        JsonNode body = objectMapper.readTree(third.getBody());
        assertThat(body.path("error").asText()).isEqualTo("license cap reached");
        assertThat(body.path("limit").asText()).isEqualTo("max_agents");
        assertThat(body.path("cap").asInt()).isEqualTo(2);
        // We installed a synthetic license, so state is ACTIVE (not ABSENT).
        assertThat(body.path("state").asText()).isEqualTo("ACTIVE");
        assertThat(body.has("message")).isTrue();
        assertThat(body.path("message").asText()).isNotBlank();

        // And the third agent must NOT be present in the registry.
        assertThat(agentRegistryService.findById("cap-it-agent-3")).isNull();
    }

    @Test
    void reRegisterAtCap_bypassesGuardAndReturns200() {
        // Fill to the cap.
        assertThat(register("cap-it-rereg-1").getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(register("cap-it-rereg-2").getStatusCode()).isEqualTo(HttpStatus.OK);

        // Re-register an existing agent — must succeed even though we're at-cap, because
        // re-registers don't grow the registry. This is the explicit design of the guard
        // placement inside the (existing == null) branch of agents.compute(...).
        ResponseEntity<String> reRegister = register("cap-it-rereg-1");
        assertThat(reRegister.getStatusCode()).isEqualTo(HttpStatus.OK);

        // And a fresh registration is still rejected.
        ResponseEntity<String> third = register("cap-it-rereg-3");
        assertThat(third.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
    }
}
