package com.mosquito.project.security;

import com.mosquito.project.config.AppConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;
import java.time.Instant;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
@DisplayName("用户内省服务测试")
class UserIntrospectionServiceTest {

    @Mock
    private RestTemplateBuilder restTemplateBuilder;

    @Mock
    private RestTemplate restTemplate;

    @Mock
    private StringRedisTemplate redisTemplate;

    @Mock
    private ValueOperations<String, String> valueOperations;

    private AppConfig appConfig;
    private UserIntrospectionService service;

    @BeforeEach
    void setUp() {
        appConfig = new AppConfig();
        appConfig.getSecurity().getIntrospection().setUrl("http://auth-server/introspect");
        appConfig.getSecurity().getIntrospection().setClientId("test-client");
        appConfig.getSecurity().getIntrospection().setClientSecret("test-secret");
        appConfig.getSecurity().getIntrospection().setTimeoutMillis(2000);
        appConfig.getSecurity().getIntrospection().setCacheTtlSeconds(60);
        appConfig.getSecurity().getIntrospection().setNegativeCacheSeconds(5);

        lenient().when(restTemplateBuilder.setConnectTimeout(any(Duration.class))).thenReturn(restTemplateBuilder);
        lenient().when(restTemplateBuilder.setReadTimeout(any(Duration.class))).thenReturn(restTemplateBuilder);
        lenient().when(restTemplateBuilder.build()).thenReturn(restTemplate);
        lenient().when(redisTemplate.opsForValue()).thenReturn(valueOperations);
    }

    @Test
    @DisplayName("授权头为空时应返回非活跃状态")
    void shouldReturnInactive_whenAuthorizationIsNull() {
        // Given
        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.empty());

        // When
        IntrospectionResponse response = service.introspect(null);

        // Then
        assertThat(response.isActive()).isFalse();
    }

    @Test
    @DisplayName("授权头为空字符串时应返回非活跃状态")
    void shouldReturnInactive_whenAuthorizationIsEmpty() {
        // Given
        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.empty());

        // When
        IntrospectionResponse response = service.introspect("");

        // Then
        assertThat(response.isActive()).isFalse();
    }

    @Test
    @DisplayName("授权头为Bearer格式时应正确提取token")
    void shouldExtractTokenCorrectly_whenBearerFormat() {
        // Given
        String token = "valid-token-123";
        IntrospectionResponse mockResponse = createActiveResponse();
        given(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(IntrospectionResponse.class)))
                .willReturn(ResponseEntity.ok(mockResponse));

        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.empty());

        // When
        IntrospectionResponse response = service.introspect("Bearer " + token);

        // Then
        assertThat(response.isActive()).isTrue();
    }

    @Test
    @DisplayName("内省URL未配置时应返回非活跃状态")
    void shouldReturnInactive_whenIntrospectionUrlNotConfigured() {
        // Given
        appConfig.getSecurity().getIntrospection().setUrl("");
        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.empty());

        // When
        IntrospectionResponse response = service.introspect("Bearer test-token");

        // Then
        assertThat(response.isActive()).isFalse();
    }

    @Test
    @DisplayName("内省响应为非活跃时应返回非活跃状态")
    void shouldReturnInactive_whenResponseIsInactive() {
        // Given
        IntrospectionResponse inactiveResponse = IntrospectionResponse.inactive();
        given(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(IntrospectionResponse.class)))
                .willReturn(ResponseEntity.ok(inactiveResponse));

        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.empty());

        // When
        IntrospectionResponse response = service.introspect("Bearer test-token");

        // Then
        assertThat(response.isActive()).isFalse();
    }

    @Test
    @DisplayName("token已过期时应返回非活跃状态")
    void shouldReturnInactive_whenTokenExpired() {
        // Given
        IntrospectionResponse expiredResponse = createActiveResponse();
        expiredResponse.setExp(Instant.now().getEpochSecond() - 100);
        given(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(IntrospectionResponse.class)))
                .willReturn(ResponseEntity.ok(expiredResponse));

        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.empty());

        // When
        IntrospectionResponse response = service.introspect("Bearer test-token");

        // Then
        assertThat(response.isActive()).isFalse();
    }

    @Test
    @DisplayName("网络异常时应返回非活跃状态并缓存")
    void shouldReturnInactive_whenNetworkError() {
        // Given
        given(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(IntrospectionResponse.class)))
                .willThrow(new ResourceAccessException("Connection refused"));

        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.empty());

        // When
        IntrospectionResponse response = service.introspect("Bearer test-token");

        // Then
        assertThat(response.isActive()).isFalse();
    }

    @Test
    @DisplayName("HTTP异常时应返回非活跃状态")
    void shouldReturnInactive_whenHttpError() {
        // Given
        given(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(IntrospectionResponse.class)))
                .willReturn(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));

        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.empty());

        // When
        IntrospectionResponse response = service.introspect("Bearer test-token");

        // Then
        assertThat(response.isActive()).isFalse();
    }

    @Test
    @DisplayName("有效token应从缓存返回")
    void shouldReturnFromCache_whenTokenCached() {
        // Given
        String cachedResponse = "{\"active\":true,\"user_id\":\"user123\"}";
        given(valueOperations.get(anyString())).willReturn(cachedResponse);

        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.of(redisTemplate));

        // When
        IntrospectionResponse response = service.introspect("Bearer cached-token");

        // Then
        assertThat(response.isActive()).isTrue();
        assertThat(response.getUserId()).isEqualTo("user123");
        verify(restTemplate, never()).postForEntity(anyString(), any(), any());
    }

    @Test
    @DisplayName("活跃响应应被缓存到Redis")
    void shouldCacheToRedis_whenActiveResponse() {
        // Given
        IntrospectionResponse activeResponse = createActiveResponse();
        activeResponse.setExp(Instant.now().getEpochSecond() + 3600);
        given(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(IntrospectionResponse.class)))
                .willReturn(ResponseEntity.ok(activeResponse));
        given(valueOperations.get(anyString())).willReturn(null);

        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.of(redisTemplate));

        // When
        service.introspect("Bearer test-token");

        // Then
        verify(valueOperations).set(anyString(), anyString(), any(Duration.class));
    }

    @Test
    @DisplayName("本地缓存应返回有效缓存项")
    void shouldReturnFromLocalCache_whenValid() {
        // Given
        IntrospectionResponse activeResponse = createActiveResponse();
        given(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(IntrospectionResponse.class)))
                .willReturn(ResponseEntity.ok(activeResponse));

        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.empty());

        // When - 第一次调用
        IntrospectionResponse response1 = service.introspect("Bearer test-token");
        // 第二次调用应使用本地缓存
        IntrospectionResponse response2 = service.introspect("Bearer test-token");

        // Then
        assertThat(response1.isActive()).isTrue();
        assertThat(response2.isActive()).isTrue();
        verify(restTemplate, times(1)).postForEntity(anyString(), any(), any());
    }

    @Test
    @DisplayName("无客户端认证配置时应发送不带认证的请求")
    void shouldSendWithoutAuth_whenNoClientConfigured() {
        // Given
        appConfig.getSecurity().getIntrospection().setClientId("");
        appConfig.getSecurity().getIntrospection().setClientSecret("");
        IntrospectionResponse activeResponse = createActiveResponse();

        ArgumentCaptor<HttpEntity> captor = ArgumentCaptor.forClass(HttpEntity.class);
        given(restTemplate.postForEntity(anyString(), captor.capture(), eq(IntrospectionResponse.class)))
                .willReturn(ResponseEntity.ok(activeResponse));

        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.empty());

        // When
        service.introspect("Bearer test-token");

        // Then
        HttpEntity<?> entity = captor.getValue();
        assertThat(entity.getHeaders().containsKey("Authorization")).isFalse();
    }

    @Test
    @DisplayName("内省响应包含完整信息时应正确解析")
    void shouldParseFullResponse_whenCompleteInfo() {
        // Given
        IntrospectionResponse fullResponse = createActiveResponse();
        fullResponse.setUserId("user-123");
        fullResponse.setTenantId("tenant-456");
        fullResponse.setRoles(java.util.List.of("admin", "user"));
        fullResponse.setScopes(java.util.List.of("read", "write"));
        given(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(IntrospectionResponse.class)))
                .willReturn(ResponseEntity.ok(fullResponse));

        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.empty());

        // When
        IntrospectionResponse response = service.introspect("Bearer test-token");

        // Then
        assertThat(response.isActive()).isTrue();
        assertThat(response.getUserId()).isEqualTo("user-123");
        assertThat(response.getTenantId()).isEqualTo("tenant-456");
        assertThat(response.getRoles()).containsExactly("admin", "user");
        assertThat(response.getScopes()).containsExactly("read", "write");
    }

    @Test
    @DisplayName("Redis读取失败时应回退到远程调用")
    void shouldFallbackToRemote_whenRedisReadFails() {
        // Given
        given(valueOperations.get(anyString())).willThrow(new RuntimeException("Redis error"));
        IntrospectionResponse activeResponse = createActiveResponse();
        given(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(IntrospectionResponse.class)))
                .willReturn(ResponseEntity.ok(activeResponse));

        service = new UserIntrospectionService(restTemplateBuilder, appConfig, Optional.of(redisTemplate));

        // When
        IntrospectionResponse response = service.introspect("Bearer test-token");

        // Then
        assertThat(response.isActive()).isTrue();
    }

    private IntrospectionResponse createActiveResponse() {
        IntrospectionResponse response = new IntrospectionResponse();
        response.setActive(true);
        response.setExp(Instant.now().getEpochSecond() + 3600);
        response.setIat(Instant.now().getEpochSecond());
        return response;
    }
}
