"""Tests for security module."""

import json
from pathlib import Path
from unittest.mock import Mock, patch, mock_open
import pytest
from chad.security import SecurityManager


class TestSecurityManager:
    """Test cases for SecurityManager."""

    def test_init_default_path(self):
        """Test initialization with default config path."""
        mgr = SecurityManager()
        assert mgr.config_path == Path.home() / ".chad.conf"

    def test_init_custom_path(self, tmp_path):
        """Test initialization with custom config path."""
        custom_path = tmp_path / "custom.conf"
        mgr = SecurityManager(custom_path)
        assert mgr.config_path == custom_path

    def test_hash_password(self):
        """Test password hashing."""
        mgr = SecurityManager()
        password = "testpassword123"
        hashed = mgr.hash_password(password)

        assert isinstance(hashed, str)
        assert len(hashed) > 0
        assert hashed != password

    def test_verify_password_correct(self):
        """Test password verification with correct password."""
        mgr = SecurityManager()
        password = "testpassword123"
        hashed = mgr.hash_password(password)

        assert mgr.verify_password(password, hashed) is True

    def test_verify_password_incorrect(self):
        """Test password verification with incorrect password."""
        mgr = SecurityManager()
        password = "testpassword123"
        hashed = mgr.hash_password(password)

        assert mgr.verify_password("wrongpassword", hashed) is False

    def test_encrypt_decrypt_value(self):
        """Test encryption and decryption of values."""
        import base64
        import bcrypt

        mgr = SecurityManager()
        password = "masterpassword"
        salt = base64.urlsafe_b64encode(bcrypt.gensalt()).decode()
        salt_bytes = base64.urlsafe_b64decode(salt.encode())
        value = "sk-test-api-key-12345"

        # Encrypt
        encrypted = mgr.encrypt_value(value, password, salt_bytes)
        assert isinstance(encrypted, str)
        assert encrypted != value

        # Decrypt
        decrypted = mgr.decrypt_value(encrypted, password, salt_bytes)
        assert decrypted == value

    def test_decrypt_with_wrong_password_raises_error(self):
        """Test that decryption with wrong password raises an error."""
        import base64
        import bcrypt

        mgr = SecurityManager()
        password = "masterpassword"
        wrong_password = "wrongpassword"
        salt = base64.urlsafe_b64encode(bcrypt.gensalt()).decode()
        salt_bytes = base64.urlsafe_b64decode(salt.encode())
        value = "sk-test-api-key-12345"

        encrypted = mgr.encrypt_value(value, password, salt_bytes)

        with pytest.raises(Exception):
            mgr.decrypt_value(encrypted, wrong_password, salt_bytes)

    def test_load_config_nonexistent_file(self, tmp_path):
        """Test loading config when file doesn't exist."""
        mgr = SecurityManager(tmp_path / "nonexistent.conf")
        config = mgr.load_config()
        assert config == {}

    def test_save_and_load_config(self, tmp_path):
        """Test saving and loading configuration."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        test_config = {
            'password_hash': 'hashed_password',
            'encryption_salt': 'test_salt',
            'api_keys': {'anthropic': 'encrypted_key'}
        }

        mgr.save_config(test_config)

        # Verify file was created and has correct permissions
        assert config_path.exists()
        assert oct(config_path.stat().st_mode)[-3:] == '600'

        # Load and verify
        loaded_config = mgr.load_config()
        assert loaded_config == test_config

    def test_is_first_run_no_config(self, tmp_path):
        """Test is_first_run when no config exists."""
        mgr = SecurityManager(tmp_path / "new.conf")
        assert mgr.is_first_run() is True

    def test_is_first_run_no_password_hash(self, tmp_path):
        """Test is_first_run when config exists but no password hash."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)
        mgr.save_config({'api_keys': {}})

        assert mgr.is_first_run() is True

    def test_is_first_run_with_password_hash(self, tmp_path):
        """Test is_first_run when password hash exists."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)
        mgr.save_config({'password_hash': 'test_hash'})

        assert mgr.is_first_run() is False

    @patch('getpass.getpass')
    def test_setup_main_password(self, mock_getpass, tmp_path):
        """Test main password setup."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        # Mock password input (password, then confirmation)
        mock_getpass.side_effect = ['testpassword123', 'testpassword123']

        password = mgr.setup_main_password()

        assert password == 'testpassword123'
        assert config_path.exists()

        # Verify config was saved correctly
        config = mgr.load_config()
        assert 'password_hash' in config
        assert 'encryption_salt' in config
        assert 'accounts' in config

        # Verify password hash is valid
        assert mgr.verify_password(password, config['password_hash']) is True

    @patch('getpass.getpass')
    def test_setup_main_password_short_allowed(self, mock_getpass, tmp_path):
        """Test main password setup allows short passwords with warning."""
        mgr = SecurityManager(tmp_path / "test.conf")

        # Mock: short password, then confirmation
        mock_getpass.side_effect = ['short', 'short']

        password = mgr.setup_main_password()
        assert password == 'short'

    @patch('getpass.getpass')
    def test_setup_main_password_empty_allowed(self, mock_getpass, tmp_path):
        """Test main password setup allows empty password with warning."""
        mgr = SecurityManager(tmp_path / "test.conf")

        # Mock: empty password, then confirmation
        mock_getpass.side_effect = ['', '']

        password = mgr.setup_main_password()
        assert password == ''

    @patch('getpass.getpass')
    def test_setup_main_password_mismatch(self, mock_getpass, tmp_path):
        """Test main password setup with mismatched passwords."""
        mgr = SecurityManager(tmp_path / "test.conf")

        # Mock: password, wrong confirmation, then correct pair
        mock_getpass.side_effect = [
            'testpassword123', 'wrongconfirm',
            'testpassword123', 'testpassword123'
        ]

        password = mgr.setup_main_password()
        assert password == 'testpassword123'

    @patch('getpass.getpass')
    def test_verify_main_password_success(self, mock_getpass, tmp_path):
        """Test successful main password verification."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        # Setup password first
        password = 'testpassword123'
        password_hash = mgr.hash_password(password)
        mgr.save_config({'password_hash': password_hash})

        # Mock getpass to return correct password
        mock_getpass.return_value = password

        verified_password = mgr.verify_main_password()
        assert verified_password == password

    @patch('getpass.getpass')
    def test_verify_main_password_retry(self, mock_getpass, tmp_path):
        """Test main password verification with retry."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        password = 'testpassword123'
        password_hash = mgr.hash_password(password)
        mgr.save_config({'password_hash': password_hash})

        # Mock: wrong password (triggers reset prompt), cancel with EOFError, then correct password
        mock_getpass.side_effect = ['wrongpassword', EOFError(), password]

        verified_password = mgr.verify_main_password()
        assert verified_password == password

    @patch('getpass.getpass')
    def test_verify_main_password_retry_after_decline(self, mock_getpass, tmp_path):
        """Test main password verification retries after cancelling reset."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        password = 'correct123'
        password_hash = mgr.hash_password(password)
        mgr.save_config({'password_hash': password_hash})

        # Mock: wrong password, cancel reset, wrong password again, cancel reset, then correct password
        mock_getpass.side_effect = ['wrong1', EOFError(), 'wrong2', KeyboardInterrupt(), password]

        verified = mgr.verify_main_password()
        assert verified == password

    @patch('getpass.getpass')
    def test_verify_main_password_reset_accepted(self, mock_getpass, tmp_path):
        """Test main password reset when user accepts."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        # Setup old password and accounts
        old_password = 'oldpassword'
        password_hash = mgr.hash_password(old_password)
        mgr.save_config({
            'password_hash': password_hash,
            'accounts': {'test-account': {'provider': 'anthropic', 'key': 'encrypted'}}
        })

        # Mock: wrong password (becomes new password candidate), then confirmation
        mock_getpass.side_effect = [
            'newpassword',  # Wrong password (becomes new password)
            'newpassword'   # Confirmation
        ]

        new_password = mgr.verify_main_password()

        assert new_password == 'newpassword'
        # Verify old accounts were deleted
        config = mgr.load_config()
        assert 'accounts' in config
        assert len(config['accounts']) == 0

    @patch('getpass.getpass')
    def test_verify_main_password_reset_confirmation_failed_then_retry(self, mock_getpass, tmp_path):
        """Test main password reset with mismatched confirmation, then retry and succeed."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        old_password = 'correct123'
        password_hash = mgr.hash_password(old_password)
        mgr.save_config({'password_hash': password_hash})

        # Mock: wrong password, mismatched confirmation, wrong password again, matching confirmation
        mock_getpass.side_effect = [
            'newpass',    # Wrong password (becomes new password candidate)
            'different',  # Mismatched confirmation - loops back
            'newpass2',   # Wrong password again (becomes new password candidate)
            'newpass2'    # Matching confirmation
        ]

        verified = mgr.verify_main_password()
        assert verified == 'newpass2'

    def test_store_and_get_api_key(self, tmp_path):
        """Test storing and retrieving API keys."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        # Setup master password
        import base64
        import bcrypt
        password = 'masterpassword'
        password_hash = mgr.hash_password(password)
        salt = base64.urlsafe_b64encode(bcrypt.gensalt()).decode()

        mgr.save_config({
            'password_hash': password_hash,
            'encryption_salt': salt,
            'api_keys': {}
        })

        # Store API key
        api_key = 'sk-test-anthropic-key-12345'
        mgr.store_api_key('anthropic', api_key, password)

        # Retrieve API key
        retrieved_key = mgr.get_api_key('anthropic', password)
        assert retrieved_key == api_key

    def test_get_api_key_nonexistent(self, tmp_path):
        """Test retrieving non-existent API key."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        import base64
        import bcrypt
        password = 'masterpassword'
        salt = base64.urlsafe_b64encode(bcrypt.gensalt()).decode()

        mgr.save_config({
            'encryption_salt': salt,
            'api_keys': {}
        })

        retrieved_key = mgr.get_api_key('nonexistent', password)
        assert retrieved_key is None

    def test_has_api_key(self, tmp_path):
        """Test checking if API key exists."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        mgr.save_config({
            'api_keys': {'anthropic': 'encrypted_key'}
        })

        assert mgr.has_api_key('anthropic') is True
        assert mgr.has_api_key('openai') is False

    def test_list_stored_providers(self, tmp_path):
        """Test listing stored providers."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        mgr.save_config({
            'api_keys': {
                'anthropic': 'key1',
                'openai': 'key2'
            }
        })

        providers = mgr.list_stored_providers()
        assert set(providers) == {'anthropic', 'openai'}

    def test_list_stored_providers_empty(self, tmp_path):
        """Test listing providers when none are stored."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)
        mgr.save_config({})

        providers = mgr.list_stored_providers()
        assert providers == []

    def test_store_and_get_account(self, tmp_path):
        """Test storing and retrieving named accounts."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        # Setup main password
        import base64
        import bcrypt
        password = 'testpassword'
        password_hash = mgr.hash_password(password)
        salt = base64.urlsafe_b64encode(bcrypt.gensalt()).decode()

        mgr.save_config({
            'password_hash': password_hash,
            'encryption_salt': salt,
            'accounts': {}
        })

        # Store account
        api_key = 'sk-test-key-12345'
        mgr.store_account('work-anthropic', 'anthropic', api_key, password)

        # Retrieve account
        account = mgr.get_account('work-anthropic', password)
        assert account is not None
        assert account['provider'] == 'anthropic'
        assert account['api_key'] == api_key

    def test_list_accounts(self, tmp_path):
        """Test listing all accounts."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        import base64
        import bcrypt
        password = 'testpassword'
        password_hash = mgr.hash_password(password)
        salt = base64.urlsafe_b64encode(bcrypt.gensalt()).decode()

        mgr.save_config({
            'password_hash': password_hash,
            'encryption_salt': salt,
            'accounts': {}
        })

        # Store multiple accounts
        mgr.store_account('work-anthropic', 'anthropic', 'key1', password)
        mgr.store_account('personal-openai', 'openai', 'key2', password)

        # List accounts
        accounts = mgr.list_accounts()
        assert len(accounts) == 2
        assert accounts['work-anthropic'] == 'anthropic'
        assert accounts['personal-openai'] == 'openai'

    def test_has_account(self, tmp_path):
        """Test checking if account exists."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        import base64
        import bcrypt
        password = 'testpassword'
        password_hash = mgr.hash_password(password)
        salt = base64.urlsafe_b64encode(bcrypt.gensalt()).decode()

        mgr.save_config({
            'password_hash': password_hash,
            'encryption_salt': salt,
            'accounts': {}
        })

        mgr.store_account('my-account', 'anthropic', 'key', password)

        assert mgr.has_account('my-account') is True
        assert mgr.has_account('nonexistent') is False

    def test_backward_compatibility_with_old_format(self, tmp_path):
        """Test that old api_keys format is still readable."""
        config_path = tmp_path / "test.conf"
        mgr = SecurityManager(config_path)

        import base64
        import bcrypt
        password = 'testpassword'
        password_hash = mgr.hash_password(password)
        salt = base64.urlsafe_b64encode(bcrypt.gensalt()).decode()
        salt_bytes = base64.urlsafe_b64decode(salt.encode())

        # Create old format config
        api_key = 'sk-old-key'
        encrypted_key = mgr.encrypt_value(api_key, password, salt_bytes)

        mgr.save_config({
            'password_hash': password_hash,
            'encryption_salt': salt,
            'api_keys': {
                'anthropic': encrypted_key
            }
        })

        # Should be able to read with new methods
        accounts = mgr.list_accounts()
        assert 'anthropic' in accounts
        assert accounts['anthropic'] == 'anthropic'

        account = mgr.get_account('anthropic', password)
        assert account is not None
        assert account['provider'] == 'anthropic'
        assert account['api_key'] == api_key
