package com.saas.admin.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.saas.admin.dto.AuthResponse;
import com.saas.admin.dto.LoginRequest;
import com.saas.admin.dto.RefreshRequest;
import com.saas.admin.dto.RegisterRequest;
import com.saas.admin.service.AuthService;
import com.saas.admin.service.RefreshTokenService;
import com.saas.admin.repository.RefreshTokenRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.transaction.annotation.Transactional;

import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@TestPropertySource(properties = {
    "security.refresh-token.expiration-days=30",
    "multitenancy.tenant-schema-prefix=tenant_"
})
@DisplayName("AuthController Integration Tests")
public class AuthControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private RefreshTokenRepository refreshTokenRepository;

    private static final String REGISTER_ENDPOINT = "/api/auth/register";
    private static final String LOGIN_ENDPOINT = "/api/auth/login";
    private static final String REFRESH_ENDPOINT = "/api/auth/refresh";
    private static final String LOGOUT_ENDPOINT = "/api/auth/logout";
    private static final String LOGOUT_ALL_ENDPOINT = "/api/auth/logout-all";

    private RegisterRequest registerRequest;
    private LoginRequest loginRequest;

    @BeforeEach
    void setUp() {
        registerRequest = new RegisterRequest();
        registerRequest.setEmail("test@example.com");
        registerRequest.setPassword("SecurePass123!");
        registerRequest.setFirstName("John");
        registerRequest.setLastName("Doe");
        registerRequest.setTenantName("Test Tenant");

        loginRequest = new LoginRequest();
        loginRequest.setEmail("test@example.com");
        loginRequest.setPassword("SecurePass123!");
    }

    @Test
    @DisplayName("Register should create user and return access + refresh tokens")
    void testRegisterSuccess() throws Exception {
        MvcResult result = mockMvc.perform(post(REGISTER_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(registerRequest)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.token").exists())
                .andExpect(jsonPath("$.refreshToken").exists())
                .andExpect(jsonPath("$.userId").exists())
                .andExpect(jsonPath("$.email").value("test@example.com"))
                .andExpect(jsonPath("$.userType").value("TENANT_USER"))
                .andReturn();

        String responseBody = result.getResponse().getContentAsString();
        AuthResponse auth = objectMapper.readValue(responseBody, AuthResponse.class);

        assertNotNull(auth.getToken());
        assertNotNull(auth.getRefreshToken());
        assertFalse(auth.getToken().isEmpty());
        assertFalse(auth.getRefreshToken().isEmpty());
    }

    @Test
    @DisplayName("Register with duplicate email should return 400")
    void testRegisterDuplicateEmail() throws Exception {
        // First registration succeeds
        mockMvc.perform(post(REGISTER_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(registerRequest)))
                .andExpect(status().isOk());

        // Second registration with same email fails
        mockMvc.perform(post(REGISTER_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(registerRequest)))
                .andExpect(status().isBadRequest());
    }

    @Test
    @DisplayName("Login should return access + refresh tokens")
    void testLoginSuccess() throws Exception {
        // Register first
        mockMvc.perform(post(REGISTER_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(registerRequest)))
                .andExpect(status().isOk());

        // Login
        MvcResult result = mockMvc.perform(post(LOGIN_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(loginRequest)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.token").exists())
                .andExpect(jsonPath("$.refreshToken").exists())
                .andExpect(jsonPath("$.email").value("test@example.com"))
                .andReturn();

        String responseBody = result.getResponse().getContentAsString();
        AuthResponse auth = objectMapper.readValue(responseBody, AuthResponse.class);

        assertNotNull(auth.getToken());
        assertNotNull(auth.getRefreshToken());
    }

    @Test
    @DisplayName("Login with invalid credentials should return 401")
    void testLoginInvalidCredentials() throws Exception {
        // Register first
        mockMvc.perform(post(REGISTER_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(registerRequest)))
                .andExpect(status().isOk());

        // Login with wrong password
        LoginRequest wrongPassword = new LoginRequest();
        wrongPassword.setEmail("test@example.com");
        wrongPassword.setPassword("WrongPassword123!");

        mockMvc.perform(post(LOGIN_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(wrongPassword)))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("Refresh endpoint should rotate tokens (revoke old, issue new)")
    void testRefreshTokenRotation() throws Exception {
        // Register and get initial tokens
        MvcResult registerResult = mockMvc.perform(post(REGISTER_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(registerRequest)))
                .andExpect(status().isOk())
                .andReturn();

        String registerBody = registerResult.getResponse().getContentAsString();
        AuthResponse initialAuth = objectMapper.readValue(registerBody, AuthResponse.class);
        String initialRefreshToken = initialAuth.getRefreshToken();
        String initialAccessToken = initialAuth.getToken();

        assertNotNull(initialRefreshToken);
        assertNotNull(initialAccessToken);

        // Refresh to get new tokens
        RefreshRequest refreshRequest = new RefreshRequest();
        refreshRequest.setRefreshToken(initialRefreshToken);

        MvcResult refreshResult = mockMvc.perform(post(REFRESH_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(refreshRequest)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.token").exists())
                .andExpect(jsonPath("$.refreshToken").exists())
                .andReturn();

        String refreshBody = refreshResult.getResponse().getContentAsString();
        AuthResponse newAuth = objectMapper.readValue(refreshBody, AuthResponse.class);
        String newRefreshToken = newAuth.getRefreshToken();
        String newAccessToken = newAuth.getToken();

        // Assert tokens are rotated (different from original)
        assertNotEquals(initialRefreshToken, newRefreshToken, "Refresh token should be rotated");
        assertNotEquals(initialAccessToken, newAccessToken, "Access token should be renewed");

        // Try to use old refresh token → should fail (was revoked)
        RefreshRequest oldRefreshRequest = new RefreshRequest();
        oldRefreshRequest.setRefreshToken(initialRefreshToken);

        mockMvc.perform(post(REFRESH_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(oldRefreshRequest)))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("Refresh with expired token should return 401")
    void testRefreshExpiredToken() throws Exception {
        RefreshRequest expiredRequest = new RefreshRequest();
        expiredRequest.setRefreshToken("expired_or_invalid_token");

        mockMvc.perform(post(REFRESH_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(expiredRequest)))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("Logout should revoke single refresh token")
    void testLogoutSingleSession() throws Exception {
        // Register and get tokens
        MvcResult registerResult = mockMvc.perform(post(REGISTER_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(registerRequest)))
                .andExpect(status().isOk())
                .andReturn();

        String registerBody = registerResult.getResponse().getContentAsString();
        AuthResponse auth = objectMapper.readValue(registerBody, AuthResponse.class);
        String refreshToken = auth.getRefreshToken();

        // Logout (revoke this specific token)
        RefreshRequest logoutRequest = new RefreshRequest();
        logoutRequest.setRefreshToken(refreshToken);

        mockMvc.perform(post(LOGOUT_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(logoutRequest)))
                .andExpect(status().isNoContent());

        // Try to use revoked refresh token → should fail
        RefreshRequest revokedRequest = new RefreshRequest();
        revokedRequest.setRefreshToken(refreshToken);

        mockMvc.perform(post(REFRESH_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(revokedRequest)))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("Logout-all should revoke all refresh tokens for user")
    void testLogoutAllSessions() throws Exception {
        // Register and get first set of tokens
        MvcResult registerResult = mockMvc.perform(post(REGISTER_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(registerRequest)))
                .andExpect(status().isOk())
                .andReturn();

        String registerBody = registerResult.getResponse().getContentAsString();
        AuthResponse firstAuth = objectMapper.readValue(registerBody, AuthResponse.class);
        String firstRefreshToken = firstAuth.getRefreshToken();
        String accessToken = firstAuth.getToken();

        // Simulate second login (get another refresh token)
        MvcResult secondLoginResult = mockMvc.perform(post(LOGIN_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(loginRequest)))
                .andExpect(status().isOk())
                .andReturn();

        String secondLoginBody = secondLoginResult.getResponse().getContentAsString();
        AuthResponse secondAuth = objectMapper.readValue(secondLoginBody, AuthResponse.class);
        String secondRefreshToken = secondAuth.getRefreshToken();

        // Both tokens should be valid before logout-all
        RefreshRequest firstRefresh = new RefreshRequest();
        firstRefresh.setRefreshToken(firstRefreshToken);

        mockMvc.perform(post(REFRESH_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(firstRefresh)))
                .andExpect(status().isOk());

        // Logout all sessions using access token
        mockMvc.perform(post(LOGOUT_ALL_ENDPOINT)
                .header("Authorization", "Bearer " + accessToken))
                .andExpect(status().isNoContent());

        // Now both refresh tokens should be revoked
        RefreshRequest firstCheck = new RefreshRequest();
        firstCheck.setRefreshToken(firstRefreshToken);

        mockMvc.perform(post(REFRESH_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(firstCheck)))
                .andExpect(status().isUnauthorized());

        RefreshRequest secondCheck = new RefreshRequest();
        secondCheck.setRefreshToken(secondRefreshToken);

        mockMvc.perform(post(REFRESH_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(secondCheck)))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("Logout-all without Authorization header should return 401")
    void testLogoutAllUnauthorized() throws Exception {
        mockMvc.perform(post(LOGOUT_ALL_ENDPOINT))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("Register → Login → Refresh → Logout flow should succeed")
    void testCompleteAuthFlow() throws Exception {
        // 1. Register
        MvcResult registerResult = mockMvc.perform(post(REGISTER_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(registerRequest)))
                .andExpect(status().isOk())
                .andReturn();

        AuthResponse registerAuth = objectMapper.readValue(
            registerResult.getResponse().getContentAsString(),
            AuthResponse.class
        );

        // 2. Login
        MvcResult loginResult = mockMvc.perform(post(LOGIN_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(loginRequest)))
                .andExpect(status().isOk())
                .andReturn();

        AuthResponse loginAuth = objectMapper.readValue(
            loginResult.getResponse().getContentAsString(),
            AuthResponse.class
        );

        // 3. Refresh
        RefreshRequest refreshRequest = new RefreshRequest();
        refreshRequest.setRefreshToken(loginAuth.getRefreshToken());

        MvcResult refreshResult = mockMvc.perform(post(REFRESH_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(refreshRequest)))
                .andExpect(status().isOk())
                .andReturn();

        AuthResponse refreshAuth = objectMapper.readValue(
            refreshResult.getResponse().getContentAsString(),
            AuthResponse.class
        );

        // 4. Logout
        RefreshRequest logoutRequest = new RefreshRequest();
        logoutRequest.setRefreshToken(refreshAuth.getRefreshToken());

        mockMvc.perform(post(LOGOUT_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(logoutRequest)))
                .andExpect(status().isNoContent());

        // 5. Verify token is revoked
        mockMvc.perform(post(REFRESH_ENDPOINT)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(logoutRequest)))
                .andExpect(status().isUnauthorized());
    }
}
