package com.openosrs.client.login;

import com.openosrs.client.core.ClientCore;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

/**
 * LoginSystemTest - Comprehensive test suite for the automated login system.
 * 
 * Tests all components of the login system including:
 * - Credential management and encryption
 * - Connection management and world selection  
 * - State tracking and transitions
 * - Complete login flow integration
 * - Error handling and edge cases
 * 
 * Designed to ensure reliable login functionality for AI agents.
 */
@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class LoginSystemTest {

    @Mock
    private ClientCore mockClientCore;
    
    private LoginManager loginManager;
    private LoginCredentials credentials;
    private GameConnectionManager connectionManager;
    private LoginStateTracker stateTracker;
    
    private Path tempCredentialsFile;
    
    @BeforeEach
    void setUp() throws Exception {
        // Create temporary file for credential testing
        tempCredentialsFile = Files.createTempFile("test-credentials", ".dat");
        
        // Initialize components
        credentials = new LoginCredentials();
        stateTracker = new LoginStateTracker();
        
        // Mock client core behavior
        when(mockClientCore.isInitialized()).thenReturn(true);
        when(mockClientCore.getPlayerState()).thenReturn(mock(Object.class));
        
        // Create login manager with mocked dependencies
        loginManager = new LoginManager(mockClientCore);
    }
    
    @AfterEach
    void tearDown() throws Exception {
        if (tempCredentialsFile != null) {
            Files.deleteIfExists(tempCredentialsFile);
        }
        
        if (loginManager != null) {
            loginManager.logout().get(5, TimeUnit.SECONDS);
        }
    }
    
    // ========== Credential Management Tests ==========
    
    @Test
    @Order(1)
    @DisplayName("Test valid credential creation")
    void testValidCredentialCreation() {
        // Test with username
        credentials.setCredentials("testuser", "password123");
        assertTrue(credentials.isValid(), "Valid username credentials should be accepted");
        assertEquals("testuser", credentials.getUsername());
        
        // Test with email
        credentials.setCredentials("test@example.com", "password123");
        assertTrue(credentials.isValid(), "Valid email credentials should be accepted");
        assertEquals("test@example.com", credentials.getUsername());
    }
    
    @Test
    @Order(2)
    @DisplayName("Test invalid credential rejection")
    void testInvalidCredentialRejection() {
        // Test null values
        credentials.setCredentials(null, "password123");
        assertFalse(credentials.isValid(), "Null username should be rejected");
        
        credentials.setCredentials("testuser", null);
        assertFalse(credentials.isValid(), "Null password should be rejected");
        
        // Test empty values
        credentials.setCredentials("", "password123");
        assertFalse(credentials.isValid(), "Empty username should be rejected");
        
        credentials.setCredentials("testuser", "");
        assertFalse(credentials.isValid(), "Empty password should be rejected");
        
        // Test short password
        credentials.setCredentials("testuser", "123");
        assertFalse(credentials.isValid(), "Short password should be rejected");
        
        // Test long password
        credentials.setCredentials("testuser", "a".repeat(25));
        assertFalse(credentials.isValid(), "Overly long password should be rejected");
    }
    
    @Test
    @Order(3)
    @DisplayName("Test credential encryption and storage")
    void testCredentialEncryption() throws Exception {
        // Set valid credentials
        credentials.setCredentials("testuser", "password123");
        assertTrue(credentials.isValid());
        
        // Save to encrypted file
        String masterPassword = "test-master-key";
        assertTrue(credentials.saveToFile(tempCredentialsFile.toString(), masterPassword),
                "Should be able to save credentials to file");
        
        // Verify file exists and is not empty
        assertTrue(Files.exists(tempCredentialsFile), "Credentials file should exist");
        assertTrue(Files.size(tempCredentialsFile) > 0, "Credentials file should not be empty");
        
        // Load credentials from file
        LoginCredentials loadedCredentials = new LoginCredentials();
        assertTrue(loadedCredentials.loadFromFile(tempCredentialsFile.toString(), masterPassword),
                "Should be able to load credentials from file");
        
        // Verify loaded credentials match original
        assertTrue(loadedCredentials.isValid(), "Loaded credentials should be valid");
        assertEquals("testuser", loadedCredentials.getUsername());
        assertEquals("password123", loadedCredentials.getPasswordAsString());
    }
    
    @Test
    @Order(4)
    @DisplayName("Test credential encryption with wrong password")
    void testCredentialDecryptionWithWrongPassword() throws Exception {
        // Save credentials with one password
        credentials.setCredentials("testuser", "password123");
        credentials.saveToFile(tempCredentialsFile.toString(), "correct-password");
        
        // Try to load with wrong password
        LoginCredentials loadedCredentials = new LoginCredentials();
        assertFalse(loadedCredentials.loadFromFile(tempCredentialsFile.toString(), "wrong-password"),
                "Should not be able to load with wrong password");
        
        assertFalse(loadedCredentials.isValid(), "Loaded credentials should be invalid");
    }
    
    // ========== State Tracking Tests ==========
    
    @Test
    @Order(5)
    @DisplayName("Test login state tracking")
    void testLoginStateTracking() {
        // Test initial state
        assertEquals(LoginState.DISCONNECTED, stateTracker.getCurrentState());
        assertFalse(stateTracker.isLoginInProgress());
        assertFalse(stateTracker.isLoggedIn());
        
        // Test state transitions
        stateTracker.setState(LoginState.CONNECTING, "Starting connection");
        assertEquals(LoginState.CONNECTING, stateTracker.getCurrentState());
        assertTrue(stateTracker.isLoginInProgress());
        
        stateTracker.setState(LoginState.AUTHENTICATING, "Sending credentials");
        assertEquals(LoginState.AUTHENTICATING, stateTracker.getCurrentState());
        assertTrue(stateTracker.isLoginInProgress());
        
        stateTracker.setState(LoginState.LOGGED_IN, "Login successful");
        assertEquals(LoginState.LOGGED_IN, stateTracker.getCurrentState());
        assertFalse(stateTracker.isLoginInProgress());
        assertTrue(stateTracker.isLoggedIn());
        
        // Verify history is recorded
        assertTrue(stateTracker.getStateHistory().size() >= 4, 
                "Should have recorded all state transitions");
    }
    
    @Test
    @Order(6)
    @DisplayName("Test state change listeners")
    void testStateChangeListeners() {
        // Set up listener
        LoginState[] receivedState = new LoginState[1];
        Consumer<LoginState> listener = state -> receivedState[0] = state;
        
        stateTracker.addStateChangeListener(listener);
        
        // Change state and verify listener was called
        stateTracker.setState(LoginState.CONNECTING);
        assertEquals(LoginState.CONNECTING, receivedState[0]);
        
        stateTracker.setState(LoginState.LOGGED_IN);
        assertEquals(LoginState.LOGGED_IN, receivedState[0]);
    }
    
    @Test
    @Order(7)
    @DisplayName("Test state history and timing")
    void testStateHistoryAndTiming() throws Exception {
        // Record some state changes with delays
        stateTracker.setState(LoginState.CONNECTING, "Start");
        Thread.sleep(100);
        
        stateTracker.setState(LoginState.AUTHENTICATING, "Auth");
        Thread.sleep(50);
        
        stateTracker.setState(LoginState.LOGGED_IN, "Success");
        
        // Verify history
        var history = stateTracker.getStateHistory();
        assertTrue(history.size() >= 4, "Should have recorded transitions"); // Including initial state
        
        // Verify timing
        assertTrue(stateTracker.getTimeInCurrentState() >= 0, 
                "Time in current state should be non-negative");
        
        assertTrue(stateTracker.getTotalLoginTime() > 0, 
                "Total login time should be positive");
    }
    
    // ========== Connection Management Tests ==========
    
    @Test
    @Order(8)
    @DisplayName("Test connection manager initialization")
    void testConnectionManagerInitialization() {
        connectionManager = new GameConnectionManager(mockClientCore);
        
        assertFalse(connectionManager.isConnected(), "Should start disconnected");
        assertEquals(-1, connectionManager.getCurrentWorld(), "Should have no world selected");
        assertEquals(-1, connectionManager.getPing(), "Should have no ping data");
    }
    
    @Test
    @Order(9)
    @DisplayName("Test world selection logic")
    void testWorldSelectionLogic() {
        connectionManager = new GameConnectionManager(mockClientCore);
        
        // Mock connection state
        // Note: In real implementation, this would test actual network connectivity
        // For now, we test the logic structure
        
        assertFalse(connectionManager.isConnected());
        
        // Test that world selection fails when not connected
        int selectedWorld = connectionManager.selectOptimalWorld();
        assertEquals(-1, selectedWorld, "World selection should fail when not connected");
    }
    
    // ========== Login Manager Integration Tests ==========
    
    @Test
    @Order(10)
    @DisplayName("Test login manager with invalid credentials")
    void testLoginManagerWithInvalidCredentials() throws Exception {
        // Test login without credentials
        CompletableFuture<Boolean> result = loginManager.login(5);
        assertFalse(result.get(10, TimeUnit.SECONDS), 
                "Login should fail without credentials");
        
        assertEquals(LoginState.FAILED, loginManager.getCurrentState());
        assertNotNull(loginManager.getLastError());
        assertFalse(loginManager.isLoggedIn());
    }
    
    @Test
    @Order(11)
    @DisplayName("Test login manager with valid credentials")
    void testLoginManagerWithValidCredentials() throws Exception {
        // Set valid credentials
        loginManager.setCredentials("testuser", "password123");
        
        // Test state change callback
        LoginState[] lastState = new LoginState[1];
        loginManager.setStateChangeCallback(state -> lastState[0] = state);
        
        // Note: This test would need actual server connectivity in real implementation
        // For now, we test the flow structure
        
        // The login will likely fail due to no actual server, but we can test the flow
        CompletableFuture<Boolean> result = loginManager.login(5);
        
        // Give it time to attempt connection
        Thread.sleep(1000);
        
        // Verify state tracking is working
        assertNotNull(lastState[0], "State change callback should have been called");
        assertNotEquals(LoginState.DISCONNECTED, loginManager.getCurrentState());
    }
    
    @Test
    @Order(12)
    @DisplayName("Test login status reporting")
    void testLoginStatusReporting() {
        // Set credentials
        loginManager.setCredentials("testuser", "password123");
        
        // Get status
        LoginManager.LoginStatus status = loginManager.getStatus();
        assertNotNull(status, "Status should not be null");
        assertNotNull(status.getState(), "Status should have a state");
        
        // Verify status fields
        assertTrue(status.getCurrentWorld() >= -1, "World should be valid or unset");
        assertTrue(status.getPing() >= -1, "Ping should be valid or unset");
        
        // Test status string representation
        String statusString = status.toString();
        assertNotNull(statusString, "Status string should not be null");
        assertTrue(statusString.contains("LoginStatus"), "Status string should contain class name");
    }
    
    @Test
    @Order(13)
    @DisplayName("Test auto-reconnect configuration")
    void testAutoReconnectConfiguration() {
        // Test auto-reconnect setting
        loginManager.setAutoReconnect(true);
        // Note: Actual reconnect testing would require network simulation
        
        loginManager.setAutoReconnect(false);
        // Verify setting can be changed without errors
        
        assertTrue(true, "Auto-reconnect configuration should work without errors");
    }
    
    @Test
    @Order(14)
    @DisplayName("Test login timeout handling")
    void testLoginTimeoutHandling() throws Exception {
        // Set credentials but use very short timeout
        loginManager.setCredentials("testuser", "password123");
        
        CompletableFuture<Boolean> result = loginManager.login(1); // 1 second timeout
        
        // Should timeout quickly
        Boolean loginResult = result.get(5, TimeUnit.SECONDS);
        assertFalse(loginResult, "Login should fail due to timeout");
        
        // Verify error message mentions timeout
        String error = loginManager.getLastError();
        assertNotNull(error, "Should have error message");
        assertTrue(error.toLowerCase().contains("timeout") || 
                  error.toLowerCase().contains("failed"), 
                  "Error should mention timeout or failure");
    }
    
    @Test
    @Order(15)
    @DisplayName("Test concurrent login attempts")
    void testConcurrentLoginAttempts() throws Exception {
        loginManager.setCredentials("testuser", "password123");
        
        // Start multiple login attempts
        CompletableFuture<Boolean> result1 = loginManager.login(5);
        CompletableFuture<Boolean> result2 = loginManager.login(5);
        
        // Both should complete (second should likely fail or be ignored)
        Boolean login1 = result1.get(10, TimeUnit.SECONDS);
        Boolean login2 = result2.get(10, TimeUnit.SECONDS);
        
        // At least one should provide a definitive result
        assertTrue(login1 != null && login2 != null, 
                "Both login attempts should return results");
    }
    
    @Test
    @Order(16)
    @DisplayName("Test logout functionality")
    void testLogoutFunctionality() throws Exception {
        // Test logout when not logged in
        CompletableFuture<Void> logoutResult = loginManager.logout();
        assertDoesNotThrow(() -> logoutResult.get(5, TimeUnit.SECONDS),
                "Logout should complete without errors even when not logged in");
        
        assertEquals(LoginState.DISCONNECTED, loginManager.getCurrentState());
        assertFalse(loginManager.isLoggedIn());
    }
    
    // ========== Integration and Error Handling Tests ==========
    
    @Test
    @Order(17)
    @DisplayName("Test complete login flow simulation")
    void testCompleteLoginFlowSimulation() throws Exception {
        // This test simulates a complete login flow with mocked components
        
        // Set up valid credentials
        loginManager.setCredentials("testuser", "password123");
        
        // Track state changes
        var stateChanges = new java.util.ArrayList<LoginState>();
        loginManager.setStateChangeCallback(stateChanges::add);
        
        // Attempt login
        CompletableFuture<Boolean> loginFuture = loginManager.login(10);
        
        // Wait a bit for state changes
        Thread.sleep(2000);
        
        // Check that state progression occurred
        assertFalse(stateChanges.isEmpty(), "Should have recorded state changes");
        
        // Verify we moved beyond initial state
        boolean hasProgressed = stateChanges.stream()
                .anyMatch(state -> state != LoginState.DISCONNECTED);
        assertTrue(hasProgressed, "Should have progressed beyond disconnected state");
        
        // Clean up
        try {
            loginFuture.get(1, TimeUnit.SECONDS);
        } catch (Exception e) {
            // Expected - no real server to connect to
        }
    }
    
    @Test
    @Order(18)
    @DisplayName("Test error recovery and resilience")
    void testErrorRecoveryAndResilience() {
        // Test that system handles various error conditions gracefully
        
        // Test with null client core (should handle gracefully)
        assertDoesNotThrow(() -> {
            LoginManager testManager = new LoginManager(null);
            // Should not crash on construction
        }, "Should handle null client core gracefully");
        
        // Test state tracker resilience
        assertDoesNotThrow(() -> {
            stateTracker.setState(null, "Test null state");
            // Should handle null state gracefully
        }, "Should handle null state gracefully");
        
        // Test credentials resilience
        assertDoesNotThrow(() -> {
            credentials.setCredentials(null, null);
            credentials.clear();
            credentials.clear(); // Double clear should be safe
        }, "Should handle multiple clears gracefully");
    }
    
    @Test
    @Order(19)
    @DisplayName("Test memory cleanup and resource management")
    void testMemoryCleanupAndResourceManagement() {
        // Test that sensitive data is properly cleared
        credentials.setCredentials("testuser", "password123");
        assertTrue(credentials.isValid());
        
        credentials.clear();
        assertFalse(credentials.isValid());
        assertNull(credentials.getUsername());
        assertNull(credentials.getPassword());
        
        // Test state tracker cleanup
        stateTracker.setState(LoginState.LOGGED_IN);
        stateTracker.reset();
        assertEquals(LoginState.DISCONNECTED, stateTracker.getCurrentState());
        
        assertTrue(true, "Memory cleanup should complete without errors");
    }
    
    @Test
    @Order(20)
    @DisplayName("Test system performance and timing")
    void testSystemPerformanceAndTiming() throws Exception {
        // Test that operations complete within reasonable time limits
        
        long startTime = System.currentTimeMillis();
        
        // Test credential operations
        credentials.setCredentials("testuser", "password123");
        credentials.saveToFile(tempCredentialsFile.toString(), "password");
        
        LoginCredentials loadedCreds = new LoginCredentials();
        loadedCreds.loadFromFile(tempCredentialsFile.toString(), "password");
        
        long credentialTime = System.currentTimeMillis() - startTime;
        assertTrue(credentialTime < 1000, 
                "Credential operations should complete quickly (was " + credentialTime + "ms)");
        
        // Test state tracking performance
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            stateTracker.setState(LoginState.CONNECTING, "Test " + i);
        }
        long trackingTime = System.currentTimeMillis() - startTime;
        assertTrue(trackingTime < 500, 
                "State tracking should be fast (was " + trackingTime + "ms for 100 operations)");
    }
}