package com.cameleer.server.app.security;

import com.cameleer.server.app.AbstractPostgresIT;
import com.cameleer.server.app.TestSecurityHelper;
import com.cameleer.server.core.security.JwtService;
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.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

import java.util.Map;

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

/**
 * Integration tests for the JWT refresh flow.
 */
class JwtRefreshIT extends AbstractPostgresIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private TestSecurityHelper securityHelper;

    @Autowired
    private JwtService jwtService;

    @BeforeEach
    void setUp() {
        // Lift max_agents cap so this IT (which registers agents per test) isn't gated
        // by license enforcement. Cap behaviour itself is exercised by AgentCapEnforcementIT.
        securityHelper.installSyntheticUnsignedLicense(Map.of("max_agents", 100));
    }

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

    private JsonNode registerAndGetTokens(String agentId) throws Exception {
        String json = """
                {
                  "instanceId": "%s",
                  "applicationId": "test-group",
                  "environmentId": "default",
                  "version": "1.0.0",
                  "routeIds": [],
                  "capabilities": {}
                }
                """.formatted(agentId);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("X-Cameleer-Protocol-Version", "1");
        headers.set("Authorization", "Bearer test-bootstrap-token");

        ResponseEntity<String> response = restTemplate.postForEntity(
                "/api/v1/agents/register",
                new HttpEntity<>(json, headers),
                String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        return objectMapper.readTree(response.getBody());
    }

    @Test
    void refreshWithValidToken_returnsNewAccessToken() throws Exception {
        JsonNode tokens = registerAndGetTokens("refresh-valid");
        String refreshToken = tokens.get("refreshToken").asText();

        String refreshBody = "{\"refreshToken\":\"" + refreshToken + "\"}";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        ResponseEntity<String> response = restTemplate.postForEntity(
                "/api/v1/agents/refresh-valid/refresh",
                new HttpEntity<>(refreshBody, headers),
                String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        JsonNode body = objectMapper.readTree(response.getBody());
        assertThat(body.get("accessToken").asText()).isNotEmpty();
        assertThat(body.get("refreshToken").asText()).isNotEmpty();
        // NB: HMAC JWTs with second-precision iat/exp are byte-identical when
        // minted for the same subject+claims within the same second, so we
        // do not assert the new token differs from the old one.
    }

    @Test
    void refreshWithAccessToken_returns401() throws Exception {
        JsonNode tokens = registerAndGetTokens("refresh-wrong-type");
        String accessToken = tokens.get("accessToken").asText();

        String refreshBody = "{\"refreshToken\":\"" + accessToken + "\"}";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        ResponseEntity<String> response = restTemplate.postForEntity(
                "/api/v1/agents/refresh-wrong-type/refresh",
                new HttpEntity<>(refreshBody, headers),
                String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
    }

    @Test
    void refreshWithMismatchedAgentId_returns401() throws Exception {
        JsonNode tokens = registerAndGetTokens("refresh-mismatch");
        String refreshToken = tokens.get("refreshToken").asText();

        String refreshBody = "{\"refreshToken\":\"" + refreshToken + "\"}";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // Use a different agent ID in the path
        ResponseEntity<String> response = restTemplate.postForEntity(
                "/api/v1/agents/wrong-agent-id/refresh",
                new HttpEntity<>(refreshBody, headers),
                String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
    }

    @Test
    void refreshWithInvalidToken_returns401() {
        String refreshBody = "{\"refreshToken\":\"invalid.jwt.token\"}";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        ResponseEntity<String> response = restTemplate.postForEntity(
                "/api/v1/agents/some-agent/refresh",
                new HttpEntity<>(refreshBody, headers),
                String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
    }

    @Test
    void newAccessTokenFromRefresh_canAccessProtectedEndpoints() throws Exception {
        JsonNode tokens = registerAndGetTokens("refresh-access-test");
        String refreshToken = tokens.get("refreshToken").asText();

        String refreshBody = "{\"refreshToken\":\"" + refreshToken + "\"}";

        HttpHeaders refreshHeaders = new HttpHeaders();
        refreshHeaders.setContentType(MediaType.APPLICATION_JSON);

        ResponseEntity<String> refreshResponse = restTemplate.postForEntity(
                "/api/v1/agents/refresh-access-test/refresh",
                new HttpEntity<>(refreshBody, refreshHeaders),
                String.class);

        assertThat(refreshResponse.getStatusCode()).isEqualTo(HttpStatus.OK);

        JsonNode refreshBody2 = objectMapper.readTree(refreshResponse.getBody());
        String newAccessToken = refreshBody2.get("accessToken").asText();

        // Use the new access token to hit an AGENT-role endpoint (heartbeat) to
        // verify the token is accepted by Spring Security. Env-scoped read
        // endpoints now require VIEWER+, so an agent token would get 403 there.
        HttpHeaders authHeaders = new HttpHeaders();
        authHeaders.set("Authorization", "Bearer " + newAccessToken);
        authHeaders.set("X-Cameleer-Protocol-Version", "1");

        ResponseEntity<String> response = restTemplate.postForEntity(
                "/api/v1/agents/refresh-access-test/heartbeat",
                new HttpEntity<>(authHeaders),
                String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}
