"""
Tests for telemetry system.

Design Standards:
- NO EMOJIS
- Test privacy guarantees
- Test opt-out functionality
"""

import json
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from deepsweep.telemetry import (
    TelemetryClient,
    TelemetryConfig,
)


@pytest.fixture
def temp_config_dir(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
    """Create temporary config directory."""
    config_dir = tmp_path / ".deepsweep"
    config_file = config_dir / "config.json"
    telemetry_disabled_file = config_dir / ".telemetry_disabled"

    # Patch module-level constants in config module
    monkeypatch.setattr("deepsweep.telemetry.config.STATE_DIR", config_dir)
    monkeypatch.setattr("deepsweep.telemetry.config.CONFIG_FILE", config_file)
    monkeypatch.setattr("deepsweep.telemetry.config.TELEMETRY_DISABLED_FILE", telemetry_disabled_file)

    # Reset events module state
    import deepsweep.telemetry.events as events
    events._shutdown = False
    events._worker_thread = None
    # Clear the queue
    while not events._event_queue.empty():
        try:
            events._event_queue.get_nowait()
        except Exception:
            break

    return config_dir


class TestTelemetryConfig:
    """Test telemetry configuration management."""

    def test_creates_config_on_first_run(self, temp_config_dir: Path) -> None:
        """Test config file is created on first run."""
        config = TelemetryConfig()

        assert config.enabled is True
        assert config.first_run is True
        assert len(config.uuid) == 36  # UUID format

        # Check file exists
        config_file = temp_config_dir / "config.json"
        assert config_file.exists()

        # Check file contents
        with config_file.open() as f:
            data = json.load(f)
            assert data["telemetry_enabled"] is True
            assert data["first_run"] is True
            assert "uuid" in data

    def test_loads_existing_config(self, temp_config_dir: Path) -> None:
        """Test loading existing config from disk."""
        # Create config file
        config_file = temp_config_dir / "config.json"
        config_dir = temp_config_dir
        config_dir.mkdir(parents=True, exist_ok=True)

        existing_data = {
            "telemetry_enabled": False,
            "uuid": "test-uuid-1234",
            "first_run": False,
        }
        with config_file.open("w") as f:
            json.dump(existing_data, f)

        # Load config
        config = TelemetryConfig()

        assert config.enabled is False
        assert config.uuid == "test-uuid-1234"
        assert config.first_run is False

    def test_enable_telemetry(self, temp_config_dir: Path) -> None:
        """Test enabling telemetry."""
        config = TelemetryConfig()
        config.disable()
        assert config.enabled is False

        config.enable()
        assert config.enabled is True

        # Check file was updated
        config_file = temp_config_dir / "config.json"
        with config_file.open() as f:
            data = json.load(f)
            assert data["telemetry_enabled"] is True

    def test_disable_telemetry(self, temp_config_dir: Path) -> None:
        """Test disabling telemetry."""
        config = TelemetryConfig()
        assert config.enabled is True

        config.disable()
        assert config.enabled is False

        # Check file was updated
        config_file = temp_config_dir / "config.json"
        with config_file.open() as f:
            data = json.load(f)
            assert data["telemetry_enabled"] is False

    def test_mark_not_first_run(self, temp_config_dir: Path) -> None:
        """Test marking first run as complete."""
        config = TelemetryConfig()
        assert config.first_run is True

        config.mark_not_first_run()
        assert config.first_run is False

        # Check file was updated
        config_file = temp_config_dir / "config.json"
        with config_file.open() as f:
            data = json.load(f)
            assert data["first_run"] is False

    def test_get_status(self, temp_config_dir: Path) -> None:
        """Test getting telemetry status."""
        config = TelemetryConfig()
        status = config.get_status()

        assert "enabled" in status
        assert "uuid" in status
        assert "config_file" in status
        assert status["enabled"] is True
        assert len(status["uuid"]) == 36

    def test_handles_corrupted_config(self, temp_config_dir: Path) -> None:
        """Test handling corrupted config file."""
        config_dir = temp_config_dir
        config_dir.mkdir(parents=True, exist_ok=True)
        config_file = config_dir / "config.json"

        # Write corrupted JSON
        config_file.write_text("{ invalid json }")

        # Should create new config
        config = TelemetryConfig()
        assert config.enabled is True
        assert len(config.uuid) == 36


class TestTelemetryClient:
    """Test telemetry client."""

    @patch("deepsweep.telemetry.events._send_to_posthog")
    def test_track_command_when_enabled(
        self, mock_send: MagicMock, temp_config_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Test tracking command when telemetry is enabled."""
        # Set API key to enable telemetry
        monkeypatch.setenv("DEEPSWEEP_POSTHOG_API_KEY", "test_key")
        monkeypatch.setenv("CI", "0")

        client = TelemetryClient()
        mock_send.return_value = True

        client.track_command(
            command="validate",
            exit_code=0,
            findings_count=5,
            pattern_count=16,
            output_format="json",
        )

        # Give background worker time to process
        import time
        time.sleep(0.1)

        # Verify PostHog send was called
        assert mock_send.called
        call_args = mock_send.call_args
        assert call_args[0][0] == "deepsweep_validate"
        assert call_args[0][1]["command"] == "validate"
        assert call_args[0][1]["exit_code"] == 0
        assert call_args[0][1]["findings_count"] == 5
        assert call_args[0][1]["pattern_count"] == 16
        assert call_args[0][1]["output_format"] == "json"

    @patch("deepsweep.telemetry.events._send_to_posthog")
    def test_track_command_when_disabled(
        self, mock_send: MagicMock, temp_config_dir: Path
    ) -> None:
        """Test tracking command when telemetry is disabled."""
        client = TelemetryClient()
        client.config.disable()

        client.track_command(command="validate", exit_code=0)

        # Give background worker time to process
        import time
        time.sleep(0.1)

        # Verify PostHog was NOT called
        assert not mock_send.called

    @patch("deepsweep.telemetry.events._send_to_posthog")
    def test_track_error(self, mock_send: MagicMock, temp_config_dir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Test tracking errors."""
        # Set API key to enable telemetry
        monkeypatch.setenv("DEEPSWEEP_POSTHOG_API_KEY", "test_key")
        monkeypatch.setenv("CI", "0")

        client = TelemetryClient()
        mock_send.return_value = True

        client.track_error(
            command="validate",
            error_type="ValidationError",
            error_message="File not found",
        )

        # Give background worker time to process
        import time
        time.sleep(0.1)

        # Verify PostHog was called
        assert mock_send.called
        call_args = mock_send.call_args

        assert call_args[0][0] == "deepsweep_error"
        assert call_args[0][1]["command"] == "validate"
        assert call_args[0][1]["error_type"] == "ValidationError"

    def test_identify(self, temp_config_dir: Path) -> None:
        """Test user identification (now a no-op)."""
        client = TelemetryClient()
        # identify() is now a no-op for backward compatibility
        client.identify()  # Should not raise

    @patch("deepsweep.telemetry.flush")
    def test_shutdown(self, mock_flush: MagicMock, temp_config_dir: Path) -> None:
        """Test shutdown flushes events."""
        client = TelemetryClient()
        client.shutdown()

        # Verify flush was called
        mock_flush.assert_called_once()

    @patch("deepsweep.telemetry.events._send_to_posthog")
    def test_marks_first_run_complete(self, mock_send: MagicMock, temp_config_dir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Test first run is marked complete after tracking."""
        # Set API key to enable telemetry
        monkeypatch.setenv("DEEPSWEEP_POSTHOG_API_KEY", "test_key")
        monkeypatch.setenv("CI", "0")

        client = TelemetryClient()
        mock_send.return_value = True
        assert client.config.first_run is True

        client.track_command(command="validate", exit_code=0)

        assert client.config.first_run is False

    def test_sanitizes_error_messages(self, temp_config_dir: Path) -> None:
        """Test error messages are not sent (privacy guarantee)."""
        client = TelemetryClient()

        # Error with home directory path - should not be sent
        error_msg = f"File not found: {Path.home()}/my-secret-project/file.txt"
        client.track_error(
            command="validate",
            error_type="ValidationError",
            error_message=error_msg,
        )

        # Error messages are not sent in new implementation (privacy)

    @patch("deepsweep.telemetry.events._send_to_posthog")
    def test_never_fails_on_telemetry_errors(
        self, mock_send: MagicMock, temp_config_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Test telemetry errors don't crash the application."""
        # Set API key to enable telemetry
        monkeypatch.setenv("DEEPSWEEP_POSTHOG_API_KEY", "test_key")
        monkeypatch.setenv("CI", "0")

        # Make PostHog raise an exception
        mock_send.side_effect = Exception("Network error")

        client = TelemetryClient()

        # Should not raise exception
        client.track_command(command="validate", exit_code=0)
        client.track_error(
            command="validate",
            error_type="ValidationError",
            error_message="Test",
        )
        client.identify()
        client.shutdown()


class TestTelemetryPrivacy:
    """Test telemetry privacy guarantees."""

    @patch("deepsweep.telemetry.events._send_to_posthog")
    def test_no_file_paths_in_telemetry(
        self, mock_send: MagicMock, temp_config_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Test file paths are not included in telemetry."""
        # Set API key to enable telemetry
        monkeypatch.setenv("DEEPSWEEP_POSTHOG_API_KEY", "test_key")
        monkeypatch.setenv("CI", "0")

        client = TelemetryClient()
        mock_send.return_value = True

        client.track_command(
            command="validate",
            exit_code=0,
            findings_count=5,
        )

        # Give background worker time to process
        import time
        time.sleep(0.1)

        if mock_send.called:
            call_args = mock_send.call_args
            properties = call_args[0][1]

            # Should not contain file paths
            assert "path" not in properties
            assert "file" not in properties
            assert "directory" not in properties

    @patch("deepsweep.telemetry.events._send_to_posthog")
    def test_no_code_content_in_telemetry(
        self, mock_send: MagicMock, temp_config_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Test code content is not included in telemetry."""
        # Set API key to enable telemetry
        monkeypatch.setenv("DEEPSWEEP_POSTHOG_API_KEY", "test_key")
        monkeypatch.setenv("CI", "0")

        client = TelemetryClient()
        mock_send.return_value = True

        client.track_command(
            command="validate",
            exit_code=0,
            findings_count=5,
        )

        # Give background worker time to process
        import time
        time.sleep(0.1)

        if mock_send.called:
            call_args = mock_send.call_args
            properties = call_args[0][1]

            # Should not contain code or finding details
            assert "code" not in properties
            assert "content" not in properties
            assert "finding" not in properties
            assert "pattern" not in properties

    @patch("deepsweep.telemetry.events._send_to_posthog")
    def test_only_aggregated_metrics(self, mock_send: MagicMock, temp_config_dir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        """Test only aggregated metrics are collected."""
        # Set API key to enable telemetry
        monkeypatch.setenv("DEEPSWEEP_POSTHOG_API_KEY", "test_key")
        monkeypatch.setenv("CI", "0")

        client = TelemetryClient()
        mock_send.return_value = True

        client.track_command(
            command="validate",
            exit_code=0,
            findings_count=5,
            pattern_count=16,
        )

        # Give background worker time to process
        import time
        time.sleep(0.1)

        if mock_send.called:
            call_args = mock_send.call_args
            properties = call_args[0][1]

            # Should only have counts, not details
            assert "findings_count" in properties
            assert properties["findings_count"] == 5
            assert "pattern_count" in properties
            assert properties["pattern_count"] == 16


class TestThreatSignalAPIContract:
    """Test threat signal implementation matches locked API contract."""

    def test_endpoint_matches_contract(self) -> None:
        """Ensure threat intel endpoint uses canonical domain."""
        from deepsweep.telemetry.threat import THREAT_INTEL_ENDPOINT

        # Enforce architecture mandate
        assert THREAT_INTEL_ENDPOINT == "https://api.deepsweep.ai/v1/signal"

        # Block deprecated domains
        assert "intel.deepsweep.ai" not in THREAT_INTEL_ENDPOINT
        assert "/ingest" not in THREAT_INTEL_ENDPOINT

    def test_threat_signal_schema_complete(self) -> None:
        """Validate ThreatSignal has all required fields for API contract."""
        from dataclasses import asdict

        from deepsweep.telemetry.threat import ThreatSignal

        signal = ThreatSignal(
            pattern_ids=["CURSOR-RULES-001"],
            cve_matches=["CVE-2025-43570"],
            severity_counts={"critical": 1, "high": 0, "medium": 0, "low": 0},
            tool_context=["cursor"],
            score=85,
            grade="B",
            finding_count=1,
            duration_ms=45,
            cli_version="1.0.1",
            python_version="3.10.0",
            os_type="darwin",
            is_ci=False,
            ci_provider=None,
        )

        payload = asdict(signal)

        # Verify all 17 required fields per API contract
        required = [
            "pattern_ids",
            "cve_matches",
            "severity_counts",
            "tool_context",
            "score",
            "grade",
            "finding_count",
            "duration_ms",
            "cli_version",
            "python_version",
            "os_type",
            "is_ci",
            "ci_provider",
            "install_id",
            "session_id",
            "timestamp",
        ]

        for field in required:
            assert field in payload, f"Missing required field: {field}"

        # Verify install_id format (64 hex chars per API contract)
        assert len(signal.install_id) == 64, "install_id must be 64 hex chars"
        assert all(c in "0123456789abcdef" for c in signal.install_id.lower())

        # Verify session_id format (16 hex chars)
        assert len(signal.session_id) == 16, "session_id must be 16 hex chars"

    def test_install_id_is_deterministic(self) -> None:
        """Install ID should be same across calls (cached)."""
        from deepsweep.telemetry.threat import _get_install_id

        id1 = _get_install_id()
        id2 = _get_install_id()

        assert id1 == id2
        assert len(id1) == 64

    def test_session_id_is_unique_per_signal(self) -> None:
        """Session ID should be different for each signal."""
        from deepsweep.telemetry.threat import ThreatSignal

        signal1 = ThreatSignal()
        signal2 = ThreatSignal()

        assert signal1.session_id != signal2.session_id
        assert len(signal1.session_id) == 16
        assert len(signal2.session_id) == 16

    @patch("deepsweep.telemetry.threat.urlopen")
    def test_signal_headers_match_contract(self, mock_urlopen: MagicMock) -> None:
        """Verify headers match API contract exactly."""
        from deepsweep.telemetry.threat import ThreatSignal, send_threat_signal

        signal = ThreatSignal(
            pattern_ids=["CURSOR-RULES-001"],
            score=85,
            grade="B",
            finding_count=1,
            duration_ms=45,
            cli_version="1.0.1",
            python_version="3.10.0",
            os_type="darwin",
            is_ci=False,
        )

        send_threat_signal(signal, offline_mode=False)

        # Give background thread time to execute
        import time

        time.sleep(0.5)

        if mock_urlopen.called:
            # Get the Request object
            request = mock_urlopen.call_args[0][0]

            # Verify all required headers
            assert request.headers["Content-type"] == "application/json"
            assert "deepsweep-cli/" in request.headers["User-agent"]
            assert "X-client-version" in request.headers
            assert "X-install-id" in request.headers

            # Verify X-Install-ID is 64 chars
            assert len(request.headers["X-install-id"]) == 64

    @patch("deepsweep.telemetry.threat.urlopen")
    def test_payload_is_direct_not_wrapped(self, mock_urlopen: MagicMock) -> None:
        """Verify payload is direct (not wrapped in event envelope)."""
        import json

        from deepsweep.telemetry.threat import ThreatSignal, send_threat_signal

        signal = ThreatSignal(
            pattern_ids=["CURSOR-RULES-001"],
            score=85,
            grade="B",
            finding_count=1,
            duration_ms=45,
            cli_version="1.0.1",
            python_version="3.10.0",
            os_type="darwin",
            is_ci=False,
        )

        send_threat_signal(signal, offline_mode=False)

        # Give background thread time to execute
        import time

        time.sleep(0.5)

        if mock_urlopen.called:
            # Get the Request object
            request = mock_urlopen.call_args[0][0]
            payload = json.loads(request.data.decode("utf-8"))

            # Should NOT have wrapper
            assert "event" not in payload
            assert "version" not in payload or payload.get("version") != "1"

            # Should have direct fields
            assert "pattern_ids" in payload
            assert "score" in payload
            assert "grade" in payload

    def test_offline_mode_skips_transmission(self) -> None:
        """Verify offline mode prevents signal transmission."""
        with patch("deepsweep.telemetry.threat._send_async") as mock_send:
            from deepsweep.telemetry.threat import ThreatSignal, send_threat_signal

            signal = ThreatSignal()
            send_threat_signal(signal, offline_mode=True)

            # Should not call send
            mock_send.assert_not_called()

    @patch("deepsweep.telemetry.threat.urlopen")
    def test_fire_and_forget_never_blocks(self, mock_urlopen: MagicMock) -> None:
        """Verify signal sending is fire-and-forget (immediate return)."""
        import time

        from deepsweep.telemetry.threat import ThreatSignal, send_threat_signal

        # Mock slow network
        def slow_response(*_args, **_kwargs):
            time.sleep(5)  # 5 second delay

        mock_urlopen.side_effect = slow_response

        signal = ThreatSignal()

        # Should return immediately (not wait 5 seconds)
        start = time.time()
        send_threat_signal(signal, offline_mode=False)
        elapsed = time.time() - start

        # Should return in < 100ms (fire-and-forget)
        assert elapsed < 0.1, "Signal sending blocked CLI execution"

    def test_ci_detection_works(self) -> None:
        """Verify CI environment detection."""
        import os

        from deepsweep.telemetry.threat import _detect_ci

        # Test GitHub Actions
        os.environ["GITHUB_ACTIONS"] = "true"
        is_ci, provider = _detect_ci()
        assert is_ci is True
        assert provider == "github"
        del os.environ["GITHUB_ACTIONS"]

        # Test GitLab CI
        os.environ["GITLAB_CI"] = "true"
        is_ci, provider = _detect_ci()
        assert is_ci is True
        assert provider == "gitlab"
        del os.environ["GITLAB_CI"]

        # Test no CI
        is_ci, provider = _detect_ci()
        # May be True if running in actual CI
        assert isinstance(is_ci, bool)

    def test_create_threat_signal_populates_all_fields(self) -> None:
        """Verify create_threat_signal populates all required fields."""
        from deepsweep.telemetry.threat import create_threat_signal

        signal = create_threat_signal(
            findings_count=5,
            score=82,
            grade="B",
            duration_ms=123,
            pattern_ids=["CURSOR-RULES-001", "MCP-POISON-001"],
            cve_matches=["CVE-2025-43570"],
            severity_counts={"critical": 1, "high": 2, "medium": 2, "low": 0},
        )

        # Verify all fields populated
        assert signal.finding_count == 5
        assert signal.score == 82
        assert signal.grade == "B"
        assert signal.duration_ms == 123
        assert len(signal.pattern_ids) == 2
        assert len(signal.cve_matches) == 1
        assert signal.severity_counts["critical"] == 1

        # Verify auto-populated fields
        assert signal.cli_version == "1.0.1"
        assert signal.python_version  # Should be populated
        assert signal.os_type in ["darwin", "linux", "windows"]
        assert isinstance(signal.is_ci, bool)
        assert len(signal.install_id) == 64
        assert len(signal.session_id) == 16
        assert signal.timestamp  # ISO 8601 format
