#!/usr/bin/env python3
"""
Security Enforcement Tests for LockStock Guard

Tests fingerprint binding and verification to prevent:
1. Stolen disk attacks (vault copied to different machine)
2. Cloud provider switches (AWS → GCP without rebinding)
3. Container lateral movement (different pod accessing secrets)

Uses mocking to simulate hardware/cloud environment changes.
"""

import pytest
import json
import sys
from pathlib import Path
from unittest.mock import patch, MagicMock
from datetime import datetime, timezone

# Add the source directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from lockstock_guard.daemon import SecretCache
from lockstock_guard.enterprise_fingerprint import EnterpriseFingerprint

# Mock fingerprint values
MOCK_AWS_FP = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"  # 64 chars
MOCK_GCP_FP = "f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5"
MOCK_BARE_METAL_FP = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
MOCK_DIFFERENT_HW = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"


class TestSecretCacheFingerprint:
    """Test fingerprint verification in SecretCache."""

    def setup_method(self):
        """Set up a fresh SecretCache for each test."""
        self.cache = SecretCache()

    def _add_secret_with_fingerprint(self, key: str, value: str, fingerprint: str):
        """Helper to add a secret with fingerprint binding."""
        self.cache._secrets[key] = value
        self.cache._fingerprints[key] = fingerprint

    def test_happy_path_same_fingerprint(self):
        """
        Scenario: Agent binds secret on Machine A, accesses it on Machine A.
        Result: Success - fingerprints match.
        """
        agent_key = "LOCKSTOCK_AGENT_TEST_007"
        secret_val = "super_secret_key"

        # Bind secret with fingerprint
        self._add_secret_with_fingerprint(agent_key, secret_val, MOCK_AWS_FP)

        # Mock EnterpriseFingerprint.generate() to return same fingerprint
        with patch.object(EnterpriseFingerprint, 'generate', return_value=MOCK_AWS_FP):
            result = self.cache.get(agent_key, verify_fingerprint=True)

        assert result is not None
        assert result == secret_val

    def test_stolen_disk_attack(self):
        """
        Scenario: Attacker copies the vault (disk) to a new machine.
        Result: Access Denied (fingerprint mismatch).
        """
        agent_key = "LOCKSTOCK_AGENT_VICTIM"
        secret_val = "stolen_secret"

        # Bind on Machine A (victim)
        self._add_secret_with_fingerprint(agent_key, secret_val, MOCK_BARE_METAL_FP)

        # Attacker runs on Machine B (different hardware)
        with patch.object(EnterpriseFingerprint, 'generate', return_value=MOCK_DIFFERENT_HW):
            result = self.cache.get(agent_key, verify_fingerprint=True)

        # Access should be denied
        assert result is None

    def test_cloud_provider_switch(self):
        """
        Scenario: Moving an image from AWS to GCP without re-binding.
        Result: Access Denied.
        """
        agent_key = "LOCKSTOCK_AGENT_CLOUD"
        secret_val = "cloud_secret"

        # Bind on AWS
        self._add_secret_with_fingerprint(agent_key, secret_val, MOCK_AWS_FP)

        # Run on GCP (different fingerprint)
        with patch.object(EnterpriseFingerprint, 'generate', return_value=MOCK_GCP_FP):
            result = self.cache.get(agent_key, verify_fingerprint=True)

        # Access should be denied
        assert result is None

    def test_container_lateral_movement(self):
        """
        Scenario: Different container/pod trying to access another pod's secrets.
        Result: Access Denied.
        """
        agent_key = "LOCKSTOCK_AGENT_POD_A"
        secret_val = "pod_a_secret"
        pod_a_fp = "container_pod_a_unique_id_" + "0" * 32

        # Bind to Pod A
        self._add_secret_with_fingerprint(agent_key, secret_val, pod_a_fp)

        # Pod B tries to access
        pod_b_fp = "container_pod_b_unique_id_" + "1" * 32
        with patch.object(EnterpriseFingerprint, 'generate', return_value=pod_b_fp):
            result = self.cache.get(agent_key, verify_fingerprint=True)

        # Access should be denied
        assert result is None

    def test_legacy_secret_no_fingerprint(self):
        """
        Scenario: Legacy secret without fingerprint binding.
        Result: Access allowed (backward compatibility) with warning.
        """
        agent_key = "LOCKSTOCK_AGENT_LEGACY"
        secret_val = "legacy_secret"

        # Add secret WITHOUT fingerprint (legacy format)
        self.cache._secrets[agent_key] = secret_val
        # No fingerprint binding

        # Should still be accessible (backward compat)
        with patch.object(EnterpriseFingerprint, 'generate', return_value=MOCK_AWS_FP):
            result = self.cache.get(agent_key, verify_fingerprint=True)

        assert result == secret_val

    def test_fingerprint_verification_disabled(self):
        """
        Scenario: Fingerprint verification explicitly disabled.
        Result: Access allowed regardless of fingerprint.
        """
        agent_key = "LOCKSTOCK_AGENT_NOVERIFY"
        secret_val = "unverified_secret"

        # Bind with one fingerprint
        self._add_secret_with_fingerprint(agent_key, secret_val, MOCK_AWS_FP)

        # Access with different fingerprint but verification disabled
        with patch.object(EnterpriseFingerprint, 'generate', return_value=MOCK_DIFFERENT_HW):
            result = self.cache.get(agent_key, verify_fingerprint=False)

        # Should succeed when verification is disabled
        assert result == secret_val

    def test_access_log_records_fingerprint_mismatch(self):
        """
        Scenario: Failed access due to fingerprint mismatch is logged.
        Result: Access log contains security event.
        """
        agent_key = "LOCKSTOCK_AGENT_LOGGED"
        secret_val = "logged_secret"

        # Bind with fingerprint
        self._add_secret_with_fingerprint(agent_key, secret_val, MOCK_AWS_FP)

        # Access with wrong fingerprint
        with patch.object(EnterpriseFingerprint, 'generate', return_value=MOCK_DIFFERENT_HW):
            result = self.cache.get(agent_key, client_info={"pid": 1234}, verify_fingerprint=True)

        # Check access log
        assert len(self.cache._access_log) > 0
        last_entry = self.cache._access_log[-1]
        assert last_entry["reason"] == "fingerprint_mismatch"
        assert last_entry["key"] == agent_key


class TestEnterpriseFingerprint:
    """Test EnterpriseFingerprint detection and generation."""

    def test_bare_metal_fallback(self):
        """Test that bare metal fingerprint is generated when not in cloud."""
        with patch.object(EnterpriseFingerprint, 'generate', wraps=EnterpriseFingerprint.generate):
            # Mock cloud detection to return unknown
            with patch('lockstock_guard.enterprise_fingerprint.CloudInstanceIdentity.get_identity', return_value=None):
                with patch('lockstock_guard.enterprise_fingerprint.ContainerDetection.is_container', return_value=False):
                    # This will either use liberty or fallback
                    fp = EnterpriseFingerprint.generate()
                    assert fp is not None
                    assert len(fp) == 64  # SHA-256 hex

    def test_debug_mode_cloud(self):
        """Test debug mode for cloud simulation."""
        fp = EnterpriseFingerprint.generate(_debug_mode='cloud')
        assert fp is not None
        assert len(fp) == 64

    def test_debug_mode_container(self):
        """Test debug mode for container simulation."""
        fp = EnterpriseFingerprint.generate(_debug_mode='container')
        assert fp is not None
        assert len(fp) == 64

    def test_fingerprint_type_detection(self):
        """Test fingerprint type description."""
        # Mock as bare metal
        with patch('lockstock_guard.enterprise_fingerprint.CloudInstanceIdentity.get_identity', return_value=None):
            with patch('lockstock_guard.enterprise_fingerprint.ContainerDetection.is_container', return_value=False):
                fp_type = EnterpriseFingerprint.get_fingerprint_type()
                assert "Bare Metal" in fp_type or "Container" in fp_type or "Cloud" in fp_type


class TestVaultJsonFormat:
    """Test the JSON format used for storing secrets with fingerprints."""

    def test_secret_data_structure(self):
        """Verify the secret data structure matches expected format."""
        secret_data = {
            "value": "test_secret_value",
            "fingerprint": MOCK_AWS_FP,
            "bound_at": datetime.now(timezone.utc).isoformat(),
            "agent_id": "agent_test_123",
        }

        # Serialize and deserialize
        json_str = json.dumps(secret_data)
        parsed = json.loads(json_str)

        assert parsed["value"] == "test_secret_value"
        assert parsed["fingerprint"] == MOCK_AWS_FP
        assert "bound_at" in parsed
        assert parsed["agent_id"] == "agent_test_123"

    def test_cache_loads_json_format(self):
        """Test that SecretCache correctly parses JSON-encoded secrets."""
        cache = SecretCache()

        # Simulate vault data structure (as loaded from disk)
        vault_data = {
            "AGENT_KEY": json.dumps({
                "value": "the_secret",
                "fingerprint": MOCK_AWS_FP,
            })
        }

        # Mock vault loading
        class MockVault:
            def _load_secrets(self):
                return vault_data

        cache.load_from_vault(MockVault())

        # Verify parsing
        assert "AGENT_KEY" in cache._secrets
        assert cache._secrets["AGENT_KEY"] == "the_secret"
        assert cache._fingerprints.get("AGENT_KEY") == MOCK_AWS_FP


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
