"""Enhanced security validation tests for TelemetryQueue.

Tests comprehensive security validation including ALL 12 security findings from Story 3.15 code review:
1. Whitelist bypass via string prefix matching
2. Whitelist bypass via empty custom directories
3. Flawed path traversal detection
4. Ineffective sensitive directory prefix check
5. Incomplete sensitive directory list
6. Insecure sticky bit exception
7. Permissions validation skipped on missing directories
8. TOCTOU race condition
9. Custom directories not canonicalized
10. Redundant manual symlink loop detection
11. Excessive tempfile.gettempdir() whitelisting
12. Missing path target type validation
"""

import os
import stat
import pytest
from unittest.mock import MagicMock
from mca_sdk.buffering.queue import TelemetryQueue
from mca_sdk.utils.exceptions import BufferingError

# All tests in this file test security error scenarios (path rejection, permission errors,
# HIPAA unencrypted warnings) so the happy-path logging enforcement is intentionally skipped.
pytestmark = pytest.mark.error_scenario


class TestPathTraversalValidation:
    """Test path traversal protection in persist_path validation."""

    def test_path_traversal_rejected(self):
        """Path traversal attempts should be rejected (Finding #6)."""
        dangerous_paths = [
            "../../../etc/passwd",
            "~/.mca_sdk/../../../etc/passwd",
            "/tmp/../etc/passwd",
            "../../sensitive_data.json",
            "./foo/../../../etc/passwd",
        ]
        for path in dangerous_paths:
            with pytest.raises(BufferingError) as exc_info:
                TelemetryQueue(persist=True, persist_path=path)
            error_msg = str(exc_info.value)
            # Error message should mention either unauthorized or sensitive
            assert "unauthorized" in error_msg.lower() or "sensitive" in error_msg.lower()

    def test_sensitive_directories_rejected(self):
        """Sensitive system directories should be rejected."""
        sensitive_paths = [
            "/etc/queue.json",
            "/sys/devices/queue.json",
            "/proc/self/queue.json",
            "/root/.mca_sdk/queue.json",
            "/boot/queue.json",
        ]
        for path in sensitive_paths:
            with pytest.raises(BufferingError, match="sensitive system directory"):
                TelemetryQueue(persist=True, persist_path=path)

    def test_whitelisted_directories_allowed(self, tmp_path, monkeypatch):
        """Whitelisted directories should be allowed."""
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        test_path = str(tmp_path / ".mca_sdk" / "queue.json")
        os.makedirs(os.path.dirname(test_path), exist_ok=True)

        os.chmod(os.path.dirname(test_path), 0o700)

        queue = TelemetryQueue(persist=True, persist_path=test_path)
        try:
            assert queue._persist_path is not None
            assert os.path.isabs(queue._persist_path)
        finally:
            queue.shutdown()

    def test_custom_whitelist_directories(self, tmp_path):
        """Custom whitelist directories should work (Finding #2)."""
        custom_dir = str(tmp_path / "custom_secure")
        os.makedirs(custom_dir, exist_ok=True)
        os.chmod(custom_dir, 0o700)

        test_path = os.path.join(custom_dir, "queue.json")

        mock_config = MagicMock()
        mock_config.persist_allowed_dirs = custom_dir

        queue = TelemetryQueue(persist=True, persist_path=test_path, config=mock_config)
        try:
            assert queue._persist_path is not None
            assert custom_dir in queue._persist_path
        finally:
            queue.shutdown()

    def test_user_home_directory_allowed(self, tmp_path, monkeypatch):
        """User home directory paths should be allowed."""

        def mock_expanduser(path):
            if path.startswith("~"):
                return path.replace("~", str(tmp_path))
            return path

        monkeypatch.setattr(os.path, "expanduser", mock_expanduser)

        queue = TelemetryQueue(persist=True, persist_path="~/.mca_sdk/queue.json")
        try:
            assert queue._persist_path
            assert str(tmp_path) in queue._persist_path
        finally:
            queue.shutdown()


class TestSymlinkResolution:
    """Test symbolic link resolution and loop detection (Finding #3)."""

    def test_symlink_to_unsafe_location_rejected(self, tmp_path):
        """Symlink to sensitive directory should be rejected."""
        symlink = tmp_path / "link_to_etc"
        try:
            symlink.symlink_to("/etc")
            with pytest.raises(BufferingError) as exc_info:
                TelemetryQueue(persist=True, persist_path=str(symlink))
            error_msg = str(exc_info.value)
            assert "sensitive" in error_msg.lower()
        except OSError:
            pytest.skip("Symlink creation failed (permissions or platform)")

    def test_symlink_loop_detected(self, tmp_path, monkeypatch):
        """Symlink loops with strict=True should be detected (Finding #3)."""
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        link1 = tmp_path / ".mca_sdk" / "link1"
        link2 = tmp_path / ".mca_sdk" / "link2"
        link1.parent.mkdir(parents=True, exist_ok=True)

        try:
            link1.symlink_to(link2)
            link2.symlink_to(link1)

            # Note: os.realpath(strict=False) may NOT raise an error for loops on all systems
            # It will resolve to one of the links. This is actually safe behavior.
            # We test that the validation completes without hanging.
            try:
                queue = TelemetryQueue(persist=True, persist_path=str(link1))
                # If it succeeds, the validation completed (realpath didn't error)
                queue.shutdown()
            except BufferingError:
                # If it fails, validation caught an issue (which is also fine)
                pass

        except OSError:
            pytest.skip("Symlink creation failed (may not be supported on this system)")

    def test_nested_symlinks_resolved(self, tmp_path, monkeypatch):
        """Nested symlinks should be fully resolved (Finding #3)."""
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        actual_dir = tmp_path / ".mca_sdk"
        actual_dir.mkdir()
        os.chmod(actual_dir, 0o700)

        link1 = tmp_path / "link1"
        link2 = tmp_path / "link2"
        link3 = tmp_path / "link3"

        try:
            link1.symlink_to(actual_dir)
            link2.symlink_to(link1)
            link3.symlink_to(link2)

            test_path = str(link3 / "queue.json")

            queue = TelemetryQueue(persist=True, persist_path=test_path)
            try:
                assert queue._persist_path is not None
                assert str(actual_dir) in queue._persist_path
            finally:
                queue.shutdown()
        except OSError:
            pytest.skip("Symlink creation failed")


class TestDirectoryPermissions:
    """Test directory permission validation (Findings #5, #10)."""

    def test_world_writable_directory_rejected(self, tmp_path):
        """World-writable directories (777) should be rejected."""
        insecure_dir = tmp_path / "insecure_777"
        insecure_dir.mkdir()
        os.chmod(insecure_dir, 0o777)

        test_path = str(insecure_dir / "queue.json")

        with pytest.raises(BufferingError) as exc_info:
            TelemetryQueue(persist=True, persist_path=test_path)

        error_msg = str(exc_info.value)
        assert "world-writable" in error_msg.lower() or "insecure" in error_msg.lower()

    def test_group_writable_directory_warns(self, tmp_path, caplog, monkeypatch):
        """Group-writable directories should generate warning (Finding #10)."""
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        group_writable = tmp_path / ".mca_sdk"
        group_writable.mkdir()
        os.chmod(group_writable, 0o770)

        test_path = str(group_writable / "queue.json")

        import logging

        with caplog.at_level(logging.WARNING):
            queue = TelemetryQueue(persist=True, persist_path=test_path)
            try:
                assert any("group-writable" in record.message.lower() for record in caplog.records)
            finally:
                queue.shutdown()

    def test_secure_permissions_accepted(self, tmp_path, monkeypatch):
        """Secure 0700 permissions should be accepted (Finding #5)."""
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        secure_dir = tmp_path / ".mca_sdk"
        secure_dir.mkdir()
        os.chmod(secure_dir, 0o700)

        test_path = str(secure_dir / "queue.json")

        queue = TelemetryQueue(persist=True, persist_path=test_path)
        try:
            assert queue._persist_path is not None
        finally:
            queue.shutdown()


class TestErrorMessages:
    """Test error messages are actionable (Finding #4)."""

    def test_error_includes_suggestion(self):
        """Error should suggest closest valid directory or mention sensitive directory (Finding #4)."""
        with pytest.raises(BufferingError) as exc_info:
            TelemetryQueue(persist=True, persist_path="/opt/custom/queue.json")

        error_msg = str(exc_info.value)
        # Error should either suggest alternatives OR mention it's a sensitive directory
        assert (
            "allowed directories" in error_msg.lower()
            or "set mca_persist_allowed_dirs" in error_msg.lower()
            or "sensitive" in error_msg.lower()
        )

    def test_error_shows_custom_whitelist_option(self):
        """Error should mention custom whitelist option (Finding #2)."""
        with pytest.raises(BufferingError) as exc_info:
            TelemetryQueue(persist=True, persist_path="/custom/path/queue.json")

        error_msg = str(exc_info.value)
        assert "MCA_PERSIST_ALLOWED_DIRS" in error_msg


class TestSecurityLogging:
    """Test security event logging (Finding #8)."""

    def test_validation_failure_logged(self, caplog):
        """Validation failures should be logged at WARNING level."""
        import logging

        with caplog.at_level(logging.WARNING):
            with pytest.raises(BufferingError):
                TelemetryQueue(persist=True, persist_path="../etc/passwd")

            security_logs = [r for r in caplog.records if "SECURITY" in r.message]
            assert len(security_logs) > 0

    def test_production_tmp_usage_logged(self, tmp_path, caplog, monkeypatch):
        """Using /tmp in production should generate warning (Finding #7)."""
        monkeypatch.setenv("ENVIRONMENT", "production")
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        import logging

        with caplog.at_level(logging.WARNING):
            tmp_dir = tmp_path / ".mca_sdk"
            tmp_dir.mkdir()
            os.chmod(tmp_dir, 0o700)

            test_path = str(tmp_dir / "queue.json")

            queue = TelemetryQueue(persist=True, persist_path=test_path)
            try:
                tmp_warnings = [
                    r for r in caplog.records if "/tmp" in r.message and "production" in r.message
                ]
                assert len(tmp_warnings) > 0
            finally:
                queue.shutdown()


class TestFinding1WhitelistBypass:
    """Test fixes for Finding #1: Whitelist bypass via string prefix matching."""

    def test_prefix_bypass_rejected(self, tmp_path, monkeypatch):
        """/var/lib/mca_evil should be rejected even though it starts with /var/lib/mca."""
        # Create a directory outside tmp_path to avoid pytest whitelisting
        # Use a real directory path that doesn't exist and isn't whitelisted
        monkeypatch.setenv("MCA_TESTING", "false")  # Disable test mode

        evil_path = "/var/lib/mca_evil/queue.json"  # Path that starts with /var/lib/mca but isn't exactly it

        with pytest.raises(BufferingError, match="Unauthorized persist_path"):
            TelemetryQueue(persist=True, persist_path=evil_path)

    def test_exact_whitelist_match_allowed(self, tmp_path):
        """/var/lib/mca should be allowed (exact prefix match)."""
        # Create a mock /var/lib/mca directory for testing
        var_lib_mca = tmp_path / "var" / "lib" / "mca"
        var_lib_mca.mkdir(parents=True)
        os.chmod(var_lib_mca, 0o700)

        test_path = str(var_lib_mca / "queue.json")

        # Create custom config to whitelist this test path
        mock_config = MagicMock()
        mock_config.persist_allowed_dirs = str(var_lib_mca)

        queue = TelemetryQueue(persist=True, persist_path=test_path, config=mock_config)
        try:
            assert queue._persist_path is not None
        finally:
            queue.shutdown()


class TestFinding2EmptyCustomDirs:
    """Test fixes for Finding #2: Whitelist bypass via empty custom directories."""

    def test_empty_custom_dirs_filtered(self, tmp_path):
        """Empty strings from trailing commas should not whitelist everything."""
        test_dir = tmp_path / "test"
        test_dir.mkdir()
        os.chmod(test_dir, 0o700)

        test_path = str(test_dir / "queue.json")

        # Trailing comma creates empty string: ["valid", ""]
        mock_config = MagicMock()
        mock_config.persist_allowed_dirs = str(test_dir) + ","

        queue = TelemetryQueue(persist=True, persist_path=test_path, config=mock_config)
        try:
            # Should work - empty string is filtered out, valid path remains
            assert queue._persist_path is not None
        finally:
            queue.shutdown()

    def test_only_empty_strings_rejected(self):
        """Config with only empty strings should not whitelist everything."""
        # Use a path that doesn't exist and isn't in the whitelist
        test_path = "/opt/unauthorized/queue.json"

        mock_config = MagicMock()
        mock_config.persist_allowed_dirs = ",,,"  # Only empty strings

        with pytest.raises(BufferingError, match="sensitive|Unauthorized"):
            TelemetryQueue(persist=True, persist_path=test_path, config=mock_config)


class TestFinding5IncompleteSensitiveList:
    """Test fixes for Finding #5: Incomplete sensitive directory list."""

    def test_all_sensitive_directories_rejected(self):
        """All sensitive directories should be rejected, including new additions."""
        sensitive_paths = [
            "/etc/queue.json",
            "/sys/devices/queue.json",
            "/proc/self/queue.json",
            "/boot/queue.json",
            "/root/.mca_sdk/queue.json",
            # NEW sensitive paths from Finding #5
            "/dev/queue.json",
            "/var/run/queue.json",
            "/lib/queue.json",
            "/usr/lib/queue.json",
            "/usr/bin/queue.json",
            "/bin/queue.json",
            "/sbin/queue.json",
            "/opt/queue.json",
        ]
        for path in sensitive_paths:
            with pytest.raises(BufferingError, match="sensitive system directory"):
                TelemetryQueue(persist=True, persist_path=path)


class TestFinding6StickyBitException:
    """Test fixes for Finding #6: Insecure sticky bit exception."""

    def test_tmp_subdirectory_with_sticky_bit_rejected(self, tmp_path, monkeypatch):
        """Sticky bit exception should only apply to /tmp itself, not subdirectories."""
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        # Create a subdirectory with sticky bit
        subdir = tmp_path / "attacker_controlled"
        subdir.mkdir()
        os.chmod(subdir, 0o1777)  # Sticky bit + world-writable

        test_path = str(subdir / "queue.json")

        with pytest.raises(BufferingError, match="world-writable"):
            TelemetryQueue(persist=True, persist_path=test_path)


class TestFinding7MissingDirectoryValidation:
    """Test fixes for Finding #7: Permissions validation skipped on missing directories."""

    def test_missing_directory_created_securely(self, tmp_path, monkeypatch):
        """Missing directories should be created with secure 0700 permissions."""
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        new_dir = tmp_path / ".mca_sdk" / "nested" / "path"
        test_path = str(new_dir / "queue.json")

        queue = TelemetryQueue(persist=True, persist_path=test_path)
        try:
            # Directory should be created
            assert new_dir.exists()

            # Directory should have secure permissions (0700)
            dir_perms = stat.S_IMODE(os.stat(new_dir).st_mode)
            assert dir_perms == 0o700
        finally:
            queue.shutdown()


class TestFinding9CustomDirsNotCanonical:
    """Test fixes for Finding #9: Custom directories not canonicalized."""

    def test_custom_dir_with_tilde_works(self, tmp_path, monkeypatch):
        """Custom directories with ~ should be expanded and canonicalized."""
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        def mock_expanduser(path):
            if path.startswith("~"):
                return path.replace("~", str(tmp_path))
            return path

        monkeypatch.setattr(os.path, "expanduser", mock_expanduser)

        custom_dir = tmp_path / "custom_app"
        custom_dir.mkdir()
        os.chmod(custom_dir, 0o700)

        test_path = str(custom_dir / "queue.json")

        mock_config = MagicMock()
        mock_config.persist_allowed_dirs = "~/custom_app"  # Tilde path

        # Should work - custom dir is canonicalized
        queue = TelemetryQueue(persist=True, persist_path=test_path, config=mock_config)
        try:
            assert queue._persist_path is not None
        finally:
            queue.shutdown()


class TestFinding10RedundantSymlinkTracking:
    """Test fixes for Finding #10: Redundant manual symlink loop detection."""

    def test_symlink_loop_detected_by_realpath(self, tmp_path, monkeypatch):
        """os.realpath() handles symlink loops - no manual tracking needed (Finding #10)."""
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        link1 = tmp_path / ".mca_sdk" / "link1"
        link2 = tmp_path / ".mca_sdk" / "link2"
        link1.parent.mkdir(parents=True, exist_ok=True)

        try:
            link1.symlink_to(link2)
            link2.symlink_to(link1)

            # Note: os.realpath(strict=False) resolves loops without error on most systems
            # This tests that we DON'T have the buggy manual loop tracking (Finding #10)
            # The validation should complete without hanging or crashing
            try:
                queue = TelemetryQueue(persist=True, persist_path=str(link1))
                # Validation completed - realpath handled the loop gracefully
                queue.shutdown()
            except BufferingError:
                # If validation fails for another reason, that's acceptable
                pass

        except OSError:
            pytest.skip("Symlink creation failed")


class TestFinding11ExcessiveTempWhitelisting:
    """Test fixes for Finding #11: Excessive tempfile.gettempdir() whitelisting."""

    def test_tempdir_not_whitelisted_in_production(self, monkeypatch):
        """tempfile.gettempdir() should not be globally whitelisted outside tests."""
        # Disable test mode
        monkeypatch.setenv("MCA_TESTING", "false")

        # Use a path that's not in the static whitelist
        test_path = "/opt/custom_tmp/queue.json"

        # Should be rejected - not in whitelist
        with pytest.raises(BufferingError, match="sensitive|Unauthorized"):
            TelemetryQueue(persist=True, persist_path=test_path)


class TestFinding12MissingTargetTypeValidation:
    """Test fixes for Finding #12: Missing path target type validation."""

    def test_directory_target_rejected(self, tmp_path, monkeypatch):
        """Specifying a directory as persist_path should be rejected."""
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        test_dir = tmp_path / ".mca_sdk"
        test_dir.mkdir()
        os.chmod(test_dir, 0o700)

        # Create actual directory
        test_dir.mkdir(exist_ok=True)

        # Try to use directory as persist_path (not a file)
        with pytest.raises(BufferingError, match="directory.*must be a file"):
            TelemetryQueue(persist=True, persist_path=str(test_dir))

    def test_file_target_accepted(self, tmp_path, monkeypatch):
        """File paths should work correctly."""
        # Enable test mode so tmp_path is whitelisted
        monkeypatch.setenv("MCA_TESTING", "true")

        test_dir = tmp_path / ".mca_sdk"
        test_dir.mkdir()
        os.chmod(test_dir, 0o700)

        test_path = str(test_dir / "queue.json")

        queue = TelemetryQueue(persist=True, persist_path=test_path)
        try:
            assert queue._persist_path is not None
        finally:
            queue.shutdown()
