"""Unit tests for telemetry module."""

from __future__ import annotations

import json
import os
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

import pytest

if TYPE_CHECKING:
    from collections.abc import Generator

from augint_library.telemetry import (
    TelemetryClient,
    _TelemetryClientHolder,
    get_telemetry_client,
    track_command_execution,
)


@pytest.fixture(autouse=True)
def reset_telemetry_client() -> Generator[None, None, None]:
    """Reset the global telemetry client between tests."""
    # Clear the singleton before each test
    _TelemetryClientHolder.client = None
    yield
    # Clear it after each test too
    _TelemetryClientHolder.client = None


@pytest.fixture
def mock_sentry() -> Generator[MagicMock, None, None]:
    """Mock sentry_sdk for tests."""
    import augint_library.telemetry

    # Save original value
    original_available = augint_library.telemetry.SENTRY_AVAILABLE

    # Set SENTRY_AVAILABLE to True using the helper
    augint_library.telemetry._set_sentry_available(True)

    # Mock sentry_sdk with all required attributes
    mock_sdk = MagicMock()
    # Ensure it has the metrics attribute
    mock_sdk.metrics = MagicMock()
    mock_sdk.metrics.incr = MagicMock()
    mock_sdk.metrics.distribution = MagicMock()
    # Add push_scope context manager
    mock_scope = MagicMock()
    mock_scope.set_extra = MagicMock()
    mock_sdk.push_scope.return_value.__enter__.return_value = mock_scope

    with patch.object(augint_library.telemetry, "sentry_sdk", mock_sdk):
        yield mock_sdk

    # Restore original value
    augint_library.telemetry._set_sentry_available(original_available)


@pytest.fixture
def temp_consent_file(tmp_path: Path) -> Generator[Path, None, None]:
    """Create temporary consent file location."""
    # Get the package name dynamically
    package_name = "augint-library"  # This matches the __package__ value
    consent_dir = tmp_path / f".{package_name}"
    consent_dir.mkdir()
    consent_file = consent_dir / "consent.json"

    with patch.object(Path, "home", return_value=tmp_path):
        yield consent_file


class TestTelemetryClient:
    """Test TelemetryClient functionality."""

    def test_disabled_by_default_without_consent(
        self, temp_consent_file: Path, mock_sentry: MagicMock
    ) -> None:
        """Test telemetry is disabled without consent."""
        with patch.dict(os.environ, {}, clear=True):
            client = TelemetryClient()
            assert not client.enabled

    @pytest.mark.skipif(os.getenv("CI") == "true", reason="Telemetry disabled in CI")
    def test_enabled_with_consent_file(
        self, temp_consent_file: Path, mock_sentry: MagicMock
    ) -> None:
        """Test telemetry is enabled when consent file exists."""
        # Create consent file
        consent_data = {
            "telemetry_enabled": True,
            "anonymous_id": "test-uuid",
        }
        temp_consent_file.write_text(json.dumps(consent_data))

        with patch.dict(os.environ, {"SENTRY_DSN": "https://test@sentry.io/123"}):
            client = TelemetryClient()
            assert client.enabled
            mock_sentry.init.assert_called_once()

    def test_disabled_by_environment_override(
        self, temp_consent_file: Path, mock_sentry: MagicMock
    ) -> None:
        """Test environment variable can disable telemetry."""
        # Create consent file saying enabled
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        with patch.dict(os.environ, {"AUGINT_LIBRARY_TELEMETRY_ENABLED": "false"}):
            client = TelemetryClient()
            assert not client.enabled

    def test_enabled_by_environment_override(
        self, temp_consent_file: Path, mock_sentry: MagicMock
    ) -> None:
        """Test environment variable can enable telemetry."""
        with patch.dict(
            os.environ,
            {
                "AUGINT_LIBRARY_TELEMETRY_ENABLED": "true",
                "SENTRY_DSN": "https://test@sentry.io/123",
            },
        ):
            client = TelemetryClient()
            assert client.enabled

    def test_disabled_in_ci_environment(self, temp_consent_file: Path) -> None:
        """Test telemetry is disabled in CI environments - no mock needed."""
        # Create consent file
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        # Test various CI environment variables
        ci_vars = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL"]
        for var in ci_vars:
            with patch.dict(os.environ, {var: "true", "SENTRY_DSN": "https://test@sentry.io/123"}):
                client = TelemetryClient()
                assert not client.enabled

    def test_set_consent_creates_file(
        self, temp_consent_file: Path, mock_sentry: MagicMock
    ) -> None:
        """Test setting consent creates consent file."""
        with patch.dict(os.environ, {"SENTRY_DSN": "https://test@sentry.io/123"}):
            client = TelemetryClient()

            # Enable telemetry
            client.set_consent(True)

            assert client.enabled
            assert temp_consent_file.exists()

            # Check file contents
            data = json.loads(temp_consent_file.read_text())
            assert data["telemetry_enabled"] is True
            assert "anonymous_id" in data
            assert "consent_timestamp" in data

    def test_set_consent_disable_when_not_initialized(self, temp_consent_file: Path) -> None:
        """Test disabling consent when telemetry was never initialized."""
        # No consent file exists initially
        client = TelemetryClient()
        assert not client.enabled
        assert not client._initialized

        # Disable telemetry (should handle gracefully even though not initialized)
        client.set_consent(False)
        assert not client.enabled
        assert temp_consent_file.exists()

        # Check file contents
        data = json.loads(temp_consent_file.read_text())
        assert data["telemetry_enabled"] is False

    @pytest.mark.skipif(os.getenv("CI") == "true", reason="Telemetry disabled in CI")
    def test_set_consent_disable(self, temp_consent_file: Path, mock_sentry: MagicMock) -> None:
        """Test disabling telemetry."""
        # First enable
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        with patch.dict(os.environ, {"SENTRY_DSN": "https://test@sentry.io/123"}):
            client = TelemetryClient()
            assert client.enabled

            # Disable
            client.set_consent(False)
            assert not client.enabled

            # Verify Sentry was disabled
            mock_sentry.init.assert_called_with(dsn=None)

    def test_anonymous_id_persistence(
        self, temp_consent_file: Path, mock_sentry: MagicMock
    ) -> None:
        """Test anonymous ID persists across instances."""
        with patch.dict(os.environ, {"SENTRY_DSN": "https://test@sentry.io/123"}):
            # First client creates ID
            client1 = TelemetryClient()
            client1.set_consent(True)
            id1 = client1._get_anonymous_id()

            # Second client reuses ID
            client2 = TelemetryClient()
            id2 = client2._get_anonymous_id()

            assert id1 == id2

    def test_scrub_sensitive_data(self, mock_sentry: MagicMock) -> None:
        """Test sensitive data scrubbing."""
        client = TelemetryClient()

        # Test event with sensitive data
        event = {
            "exception": {
                "values": [
                    {
                        "stacktrace": {
                            "frames": [
                                {
                                    "filename": "/home/user/project/file.py",
                                    "vars": {"password": "secret"},
                                }
                            ]
                        }
                    }
                ]
            },
            "extra": {
                "api_key": "secret-key",
                "user_path": "/home/user/data",
                "safe_data": "this is ok",
            },
            "server_name": "my-hostname",
            "request": {"url": "http://example.com"},
        }

        scrubbed = client._scrub_sensitive_data(event, {})

        # Check sensitive data was removed
        assert "vars" not in scrubbed["exception"]["values"][0]["stacktrace"]["frames"][0]
        assert scrubbed["extra"]["api_key"] == "[REDACTED]"
        assert scrubbed["extra"]["user_path"] == "[REDACTED]"  # Changed from [PATH] to [REDACTED]
        assert scrubbed["extra"]["safe_data"] == "this is ok"
        assert "server_name" not in scrubbed
        assert "request" not in scrubbed

    def test_scrub_sensitive_data_edge_cases(self) -> None:
        """Test sensitive data scrubbing with edge cases."""
        client = TelemetryClient()

        # Test with missing exception
        event1 = {"extra": {"key": "value"}}
        scrubbed1 = client._scrub_sensitive_data(event1, {})
        assert "exception" not in scrubbed1

        # Test with non-dict exception
        event2 = {"exception": "not a dict"}
        scrubbed2 = client._scrub_sensitive_data(event2, {})
        assert scrubbed2["exception"] == "not a dict"

        # Test with missing values in exception
        event3 = {"exception": {"no_values": True}}
        scrubbed3 = client._scrub_sensitive_data(event3, {})
        assert scrubbed3["exception"]["no_values"] is True

        # Test with user data
        event4 = {"user": {"id": "user123", "email": "test@example.com"}}
        scrubbed4 = client._scrub_sensitive_data(event4, {})
        assert "email" not in scrubbed4["user"]
        assert scrubbed4["user"]["id"] == client._get_anonymous_id()

        # Test with breadcrumbs
        event5 = {
            "breadcrumbs": {
                "values": [{"data": {"password": "secret", "safe": "ok"}}, {"no_data": True}]
            }
        }
        scrubbed5 = client._scrub_sensitive_data(event5, {})
        assert scrubbed5["breadcrumbs"]["values"][0]["data"]["password"] == "[REDACTED]"
        assert scrubbed5["breadcrumbs"]["values"][0]["data"]["safe"] == "ok"

        # Test with non-dict breadcrumbs
        event6 = {"breadcrumbs": "not a dict"}
        scrubbed6 = client._scrub_sensitive_data(event6, {})
        assert scrubbed6["breadcrumbs"] == "not a dict"

    def test_anonymize_path(self) -> None:
        """Test path anonymization."""
        client = TelemetryClient()

        # Test with site-packages path
        path1 = "/home/user/.local/lib/python3.9/site-packages/mylib/file.py"
        anon1 = client._anonymize_path(path1)
        assert "/home/user" not in anon1
        assert anon1 == "mylib/file.py"  # Shows path after site-packages

        # Test with augint_library path
        path2 = "/Users/john.doe/projects/augint_library/src/file.py"
        anon2 = client._anonymize_path(path2)
        assert "john.doe" not in anon2
        assert anon2 == "augint_library/src/file.py"

        # Test with other path - should just show filename
        path3 = "/tmp/pytest-123/test_file.py"
        anon3 = client._anonymize_path(path3)
        assert anon3 == "test_file.py"

    def test_scrub_dict(self) -> None:
        """Test dictionary scrubbing with nested structures."""
        client = TelemetryClient()

        # Test with nested sensitive data
        data = {
            "safe_data": "safe_value",  # Changed from safe_key to avoid 'key' in name
            "password": "secret123",
            "api_key": "key123",
            "token": "token123",
            "nested": {
                "secret": "nested_secret",
                "safe": "nested_safe",
                "deeper": {"auth_token": "deep_token"},
            },
            "path": "/home/user/file.txt",
            "non_dict_value": 123,
        }

        scrubbed = client._scrub_dict(data)

        assert scrubbed["safe_data"] == "safe_value"
        assert scrubbed["password"] == "[REDACTED]"
        assert scrubbed["api_key"] == "[REDACTED]"
        assert scrubbed["token"] == "[REDACTED]"
        assert scrubbed["nested"]["secret"] == "[REDACTED]"
        assert scrubbed["nested"]["safe"] == "nested_safe"
        assert scrubbed["nested"]["deeper"]["auth_token"] == "[REDACTED]"
        assert scrubbed["path"] == "[REDACTED]"
        assert scrubbed["non_dict_value"] == 123

    @pytest.mark.skipif(os.getenv("CI") == "true", reason="Telemetry disabled in CI")
    def test_track_command(self, mock_sentry: MagicMock, temp_consent_file: Path) -> None:
        """Test command tracking."""
        # Enable telemetry
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        with patch.dict(os.environ, {"SENTRY_DSN": "https://test@sentry.io/123"}):
            client = TelemetryClient()

            # Track command
            client.track_command("test_command", success=True, duration=1.5)

            # Verify Sentry calls
            mock_sentry.capture_message.assert_called_once()
            mock_sentry.metrics.incr.assert_called_once()
            mock_sentry.metrics.distribution.assert_called_once()

    def test_track_command_disabled(self, mock_sentry: MagicMock) -> None:
        """Test command tracking when disabled."""
        client = TelemetryClient()
        client.track_command("test_command", success=True)

        # Should not call Sentry
        mock_sentry.capture_message.assert_not_called()

    @pytest.mark.skipif(os.getenv("CI") == "true", reason="Telemetry disabled in CI")
    def test_track_error(self, mock_sentry: MagicMock, temp_consent_file: Path) -> None:
        """Test error tracking."""
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        with patch.dict(os.environ, {"SENTRY_DSN": "https://test@sentry.io/123"}):
            client = TelemetryClient()

            # Track error
            error = ValueError("Test error")
            client.track_error(error, {"context": "test"})

            mock_sentry.capture_exception.assert_called_once()

    @pytest.mark.skipif(os.getenv("CI") == "true", reason="Telemetry disabled in CI")
    def test_track_metric(self, mock_sentry: MagicMock, temp_consent_file: Path) -> None:
        """Test metric tracking."""
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        with patch.dict(os.environ, {"SENTRY_DSN": "https://test@sentry.io/123"}):
            client = TelemetryClient()

            # Track metric
            client.track_metric("test_metric", 42.0, unit="ms", tags={"type": "test"})

            mock_sentry.metrics.distribution.assert_called_once_with(
                key="custom.test_metric", value=42.0, unit="ms", tags={"type": "test"}
            )

    def test_no_sentry_available(self, temp_consent_file: Path) -> None:
        """Test behavior when sentry-sdk is not installed."""
        import augint_library.telemetry

        # Save original value
        original_available = augint_library.telemetry.SENTRY_AVAILABLE

        # Set SENTRY_AVAILABLE to False
        augint_library.telemetry._set_sentry_available(False)

        try:
            consent_data = {"telemetry_enabled": True}
            temp_consent_file.write_text(json.dumps(consent_data))

            client = TelemetryClient()
            assert not client.enabled

            # Methods should not raise errors
            client.track_command("test", True)
            client.track_error(ValueError("test"))
            client.track_metric("test", 1.0)
            client.flush()
        finally:
            # Restore original value
            augint_library.telemetry._set_sentry_available(original_available)

    def test_all_tracking_methods_in_ci(self, temp_consent_file: Path) -> None:
        """Test all tracking methods work safely when telemetry is disabled in CI."""
        # Create consent but set CI environment
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        with patch.dict(os.environ, {"CI": "true", "SENTRY_DSN": "https://test@sentry.io/123"}):
            client = TelemetryClient()
            assert not client.enabled  # Should be disabled in CI

            # Test all tracking methods - they should not raise errors
            client.track_command("test_command", success=True, duration=1.5)
            client.track_command("test_command", success=False)  # Without duration

            error = ValueError("Test error")
            client.track_error(error, {"context": "test"})
            client.track_error(error)  # Without context

            client.track_metric("test_metric", 42.0, unit="ms", tags={"type": "test"})
            client.track_metric("test_metric", 42.0)  # Without unit and tags

            client.flush(timeout=1.0)
            client.flush()  # Default timeout

            # Test set_consent methods
            client.set_consent(True)
            # Note: set_consent(True) overrides CI detection - this is by design
            # to allow users to explicitly enable telemetry even in CI if needed
            client.set_consent(False)
            assert not client.enabled

    def test_tracking_with_mock_covers_implementation(
        self, mock_sentry: MagicMock, temp_consent_file: Path
    ) -> None:
        """Test tracking method implementations with mocking to improve coverage."""
        # Create consent file
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        # Don't set CI environment - we want to test the enabled code paths
        with patch.dict(os.environ, {"SENTRY_DSN": "https://test@sentry.io/123"}, clear=True):
            client = TelemetryClient()

            # If not enabled for some reason, force enable it for coverage
            if not client.enabled:
                client._enabled = True
                client._initialized = True

            # Test track_command with all branches
            client.track_command("test_command", success=True, duration=1.5)
            mock_sentry.capture_message.assert_called()
            mock_sentry.metrics.incr.assert_called()
            mock_sentry.metrics.distribution.assert_called()

            # Reset mocks
            mock_sentry.reset_mock()

            # Test track_command without duration
            client.track_command("test_command", success=False)
            mock_sentry.capture_message.assert_called()
            mock_sentry.metrics.incr.assert_called()

            # Test track_error with context
            error = ValueError("Test error")
            client.track_error(error, {"context": "test"})
            mock_sentry.capture_exception.assert_called_once()

            # Reset and test without context
            mock_sentry.reset_mock()
            client.track_error(error)
            mock_sentry.capture_exception.assert_called_once()

            # Test track_metric with all parameters
            client.track_metric("test_metric", 42.0, unit="ms", tags={"type": "test"})
            mock_sentry.metrics.distribution.assert_called_with(
                key="custom.test_metric", value=42.0, unit="ms", tags={"type": "test"}
            )

            # Test track_metric with minimal parameters
            client.track_metric("simple_metric", 10.0)
            mock_sentry.metrics.distribution.assert_called_with(
                key="custom.simple_metric", value=10.0, tags={}
            )

            # Test flush
            client.flush(timeout=2.0)
            mock_sentry.flush.assert_called_with(timeout=2.0)

            # Test flush with default timeout
            mock_sentry.reset_mock()
            client.flush()
            mock_sentry.flush.assert_called_with(timeout=2.0)

    def test_get_anonymous_id_creates_new(self, temp_consent_file: Path) -> None:
        """Test anonymous ID generation when none exists."""
        # Ensure no consent file exists
        if temp_consent_file.exists():
            temp_consent_file.unlink()

        client = TelemetryClient()
        anon_id = client._get_anonymous_id()

        # Should be a valid UUID string
        assert anon_id
        assert isinstance(anon_id, str)
        assert len(anon_id) == 36  # Standard UUID format

    def test_should_enable_telemetry_env_override(self, temp_consent_file: Path) -> None:
        """Test environment variable override logic."""
        # Create a consent file so the default would be True
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        client = TelemetryClient()

        # Test with various env values
        with patch.dict(os.environ, {"AUGINT_LIBRARY_TELEMETRY_ENABLED": "true"}, clear=True):
            assert client._should_enable_telemetry() is True

        with patch.dict(os.environ, {"AUGINT_LIBRARY_TELEMETRY_ENABLED": "false"}, clear=True):
            assert client._should_enable_telemetry() is False

        # The implementation only checks for "true" and "false" strings
        with patch.dict(os.environ, {"AUGINT_LIBRARY_TELEMETRY_ENABLED": "1"}, clear=True):
            # Should fall back to consent file check
            assert client._should_enable_telemetry() is True

        with patch.dict(os.environ, {"AUGINT_LIBRARY_TELEMETRY_ENABLED": "0"}, clear=True):
            # Should fall back to consent file check
            assert client._should_enable_telemetry() is True

        with patch.dict(os.environ, {"AUGINT_LIBRARY_TELEMETRY_ENABLED": "invalid"}, clear=True):
            # Invalid values should fall back to consent file check
            assert client._should_enable_telemetry() is True

    def test_initialize_sentry_error_handling(
        self, mock_sentry: MagicMock, temp_consent_file: Path
    ) -> None:
        """Test error handling during Sentry initialization."""
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        # Mock sentry.init to raise an exception
        mock_sentry.init.side_effect = Exception("Sentry init failed")

        with patch.dict(os.environ, {"SENTRY_DSN": "https://test@sentry.io/123"}, clear=True):
            client = TelemetryClient()
            # Should handle the error gracefully
            assert not client.enabled
            assert not client._initialized

    def test_check_stored_consent_invalid_json(self, temp_consent_file: Path) -> None:
        """Test handling of invalid JSON in consent file."""
        # Write invalid JSON
        temp_consent_file.write_text("not valid json")

        client = TelemetryClient()
        # Should handle the error gracefully and return False
        result = client._check_stored_consent()
        assert result is False

    def test_consent_file_os_error(self, temp_consent_file: Path) -> None:
        """Test handling of OS errors when reading consent file."""
        # Create consent file
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        client = TelemetryClient()

        # Mock open to raise OSError
        with patch.object(Path, "open", side_effect=OSError("File locked")):
            result = client._check_stored_consent()
            assert result is False

    def test_get_anonymous_id_with_existing_file_os_error(self, temp_consent_file: Path) -> None:
        """Test anonymous ID generation when file exists but can't be read."""
        # Create consent file
        consent_data = {"telemetry_enabled": True, "anonymous_id": "existing-id"}
        temp_consent_file.write_text(json.dumps(consent_data))

        client = TelemetryClient()

        # Mock open to raise OSError
        with patch.object(Path, "open", side_effect=OSError("File locked")):
            anon_id = client._get_anonymous_id()
            # Should generate a new ID when file can't be read
            assert anon_id
            assert anon_id != "existing-id"
            assert len(anon_id) == 36  # UUID format


class TestTelemetryDecorator:
    """Test track_command_execution decorator."""

    def test_decorator_success(self, mock_sentry: MagicMock, temp_consent_file: Path) -> None:
        """Test decorator tracks successful execution."""
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        with patch.dict(os.environ, {"SENTRY_DSN": "https://test@sentry.io/123"}):
            # Create a mock client that's enabled
            mock_client = MagicMock()
            mock_client.enabled = True
            mock_client.track_command = MagicMock()

            with patch("augint_library.telemetry.get_telemetry_client", return_value=mock_client):

                @track_command_execution
                def test_command():
                    return "success"

                result = test_command()
                assert result == "success"

                # Verify tracking
                mock_client.track_command.assert_called_once()
                call_args = mock_client.track_command.call_args
                assert call_args[0][0] == "test_command"
                assert call_args[1]["success"] is True
                assert "duration" in call_args[1]

    def test_decorator_failure(self, mock_sentry: MagicMock, temp_consent_file: Path) -> None:
        """Test decorator tracks failed execution."""
        consent_data = {"telemetry_enabled": True}
        temp_consent_file.write_text(json.dumps(consent_data))

        with patch.dict(os.environ, {"SENTRY_DSN": "https://test@sentry.io/123"}):
            # Create a mock client that's enabled
            mock_client = MagicMock()
            mock_client.enabled = True
            mock_client.track_command = MagicMock()
            mock_client.track_error = MagicMock()

            with patch("augint_library.telemetry.get_telemetry_client", return_value=mock_client):

                @track_command_execution
                def failing_command():
                    raise ValueError("Test error")

                with pytest.raises(ValueError, match="Test error"):
                    failing_command()

                # Verify tracking
                mock_client.track_command.assert_called_once()
                call_args = mock_client.track_command.call_args
                assert call_args[0][0] == "failing_command"
                assert call_args[1]["success"] is False
                assert "duration" in call_args[1]

                # Verify error was captured
                mock_client.track_error.assert_called_once()


class TestGlobalTelemetryClient:
    """Test global telemetry client singleton."""

    def test_singleton_behavior(self) -> None:
        """Test get_telemetry_client returns same instance."""
        client1 = get_telemetry_client()
        client2 = get_telemetry_client()
        assert client1 is client2
