"""Unit tests for debug logging functionality."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from teams_phone.infrastructure.debug_logger import (
    DebugLogger,
    mask_headers,
    mask_secrets,
    mask_upn,
)


class TestMaskSecrets:
    """Tests for secret masking function."""

    @pytest.mark.unit
    def test_masks_bearer_token(self) -> None:
        """Bearer tokens are masked."""
        value = "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.payload.signature"
        result = mask_secrets(value)
        assert result == "Authorization: Bearer [REDACTED]"
        assert "eyJ0eXAi" not in result

    @pytest.mark.unit
    def test_masks_multiple_bearer_tokens(self) -> None:
        """Multiple Bearer tokens in same string are all masked."""
        value = "Token1: Bearer abc123 Token2: Bearer xyz789"
        result = mask_secrets(value)
        assert result == "Token1: Bearer [REDACTED] Token2: Bearer [REDACTED]"

    @pytest.mark.unit
    def test_masks_client_secret(self) -> None:
        """Client secrets are masked."""
        value = 'client_secret: "my-super-secret-value"'
        result = mask_secrets(value)
        assert "[REDACTED]" in result
        assert "my-super-secret-value" not in result

    @pytest.mark.unit
    def test_preserves_non_secret_values(self) -> None:
        """Non-secret values are preserved."""
        value = "https://graph.microsoft.com/v1.0/users?$top=100"
        result = mask_secrets(value)
        assert result == value

    @pytest.mark.unit
    def test_empty_string(self) -> None:
        """Empty string returns empty string."""
        result = mask_secrets("")
        assert result == ""


class TestMaskUpn:
    """Tests for UPN masking function."""

    @pytest.mark.unit
    def test_masks_standard_upn(self) -> None:
        """Standard UPN is masked correctly."""
        result = mask_upn("john.doe@contoso.com")
        assert result == "j***@contoso.com"

    @pytest.mark.unit
    def test_masks_single_char_local_part(self) -> None:
        """Single character local part is masked."""
        result = mask_upn("j@contoso.com")
        assert result == "j***@contoso.com"

    @pytest.mark.unit
    def test_masks_empty_local_part(self) -> None:
        """Empty local part is handled."""
        result = mask_upn("@contoso.com")
        assert result == "***@contoso.com"

    @pytest.mark.unit
    def test_masks_non_upn_format(self) -> None:
        """Non-UPN strings are masked."""
        result = mask_upn("username")
        assert result == "u***"

    @pytest.mark.unit
    def test_masks_single_char_non_upn(self) -> None:
        """Single character non-UPN is masked."""
        result = mask_upn("a")
        assert result == "***"

    @pytest.mark.unit
    def test_masks_empty_string(self) -> None:
        """Empty string returns masked."""
        result = mask_upn("")
        assert result == "***"


class TestMaskHeaders:
    """Tests for HTTP header masking function."""

    @pytest.mark.unit
    def test_masks_authorization_header(self) -> None:
        """Authorization header is masked."""
        headers = {
            "Authorization": "Bearer abc123xyz",
            "Content-Type": "application/json",
        }
        result = mask_headers(headers)
        assert result["Authorization"] == "Bearer [REDACTED]"
        assert result["Content-Type"] == "application/json"

    @pytest.mark.unit
    def test_case_insensitive_authorization(self) -> None:
        """Authorization header masking is case-insensitive."""
        headers = {"authorization": "Bearer token123"}
        result = mask_headers(headers)
        assert result["authorization"] == "Bearer [REDACTED]"

    @pytest.mark.unit
    def test_preserves_other_headers(self) -> None:
        """Non-sensitive headers are preserved."""
        headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "X-Custom-Header": "custom-value",
        }
        result = mask_headers(headers)
        assert result == headers

    @pytest.mark.unit
    def test_empty_headers(self) -> None:
        """Empty headers dict returns empty dict."""
        result = mask_headers({})
        assert result == {}


class TestDebugLoggerInit:
    """Tests for DebugLogger initialization."""

    @pytest.mark.unit
    def test_creates_correlation_id(self, tmp_path: Path) -> None:
        """DebugLogger creates correlation ID on init."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)
        assert logger.correlation_id is not None
        assert len(logger.correlation_id) == 8  # First 8 chars of UUID

    @pytest.mark.unit
    def test_stores_log_path(self, tmp_path: Path) -> None:
        """DebugLogger stores log path."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)
        assert logger.log_path == log_path

    @pytest.mark.unit
    def test_creates_parent_directory(self, tmp_path: Path) -> None:
        """DebugLogger creates parent directory if it doesn't exist."""
        log_path = tmp_path / "subdir" / "debug.log"
        DebugLogger(log_path)
        assert log_path.parent.exists()


class TestDebugLoggerLogCommandStart:
    """Tests for DebugLogger.log_command_start method."""

    @pytest.mark.unit
    def test_logs_command_start(self, tmp_path: Path) -> None:
        """log_command_start writes correct JSON entry."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_command_start("users list", tenant="contoso")

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["event"] == "command_start"
        assert entry["level"] == "INFO"
        assert entry["command"] == "users list"
        assert entry["tenant"] == "contoso"
        assert entry["correlation_id"] == logger.correlation_id
        assert "ts" in entry

    @pytest.mark.unit
    def test_logs_command_start_without_tenant(self, tmp_path: Path) -> None:
        """log_command_start works without tenant."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_command_start("users list")

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["event"] == "command_start"
        assert entry["command"] == "users list"
        assert "tenant" not in entry


class TestDebugLoggerLogCommandComplete:
    """Tests for DebugLogger.log_command_complete method."""

    @pytest.mark.unit
    def test_logs_command_complete(self, tmp_path: Path) -> None:
        """log_command_complete writes correct JSON entry."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_command_complete(exit_code=0, duration_ms=1234.567)

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["event"] == "command_complete"
        assert entry["level"] == "INFO"
        assert entry["exit_code"] == 0
        assert entry["duration_ms"] == 1234.6  # Rounded to 1 decimal
        assert entry["correlation_id"] == logger.correlation_id


class TestDebugLoggerLogGraphRequest:
    """Tests for DebugLogger.log_graph_request method."""

    @pytest.mark.unit
    def test_logs_graph_request(self, tmp_path: Path) -> None:
        """log_graph_request writes correct JSON entry."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        start_time = logger.log_graph_request(
            "GET",
            "https://graph.microsoft.com/v1.0/users",
        )

        assert isinstance(start_time, float)
        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["event"] == "graph_request"
        assert entry["level"] == "DEBUG"
        assert entry["method"] == "GET"
        assert entry["url"] == "https://graph.microsoft.com/v1.0/users"

    @pytest.mark.unit
    def test_logs_graph_request_with_body_size(self, tmp_path: Path) -> None:
        """log_graph_request includes body_size when provided."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_graph_request(
            "POST",
            "https://graph.microsoft.com/v1.0/users",
            body_size=1024,
        )

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["body_size"] == 1024

    @pytest.mark.unit
    def test_masks_secrets_in_url(self, tmp_path: Path) -> None:
        """log_graph_request masks secrets in URL."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        # URL with Bearer token (unlikely but should be masked)
        logger.log_graph_request(
            "GET",
            "https://example.com?token=Bearer abc123",
        )

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert "abc123" not in entry["url"]
        assert "[REDACTED]" in entry["url"]


class TestDebugLoggerLogGraphResponse:
    """Tests for DebugLogger.log_graph_response method."""

    @pytest.mark.unit
    def test_logs_graph_response(self, tmp_path: Path) -> None:
        """log_graph_response writes correct JSON entry."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        import time

        start_time = time.perf_counter()
        time.sleep(0.01)  # Small delay for measurable duration
        logger.log_graph_response(200, start_time)

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["event"] == "graph_response"
        assert entry["level"] == "DEBUG"
        assert entry["status"] == 200
        assert entry["duration_ms"] >= 10  # At least 10ms from sleep

    @pytest.mark.unit
    def test_logs_graph_response_with_items_count(self, tmp_path: Path) -> None:
        """log_graph_response includes items_count when provided."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        import time

        start_time = time.perf_counter()
        logger.log_graph_response(200, start_time, items_count=50)

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["items_count"] == 50


class TestDebugLoggerLogRetry:
    """Tests for DebugLogger.log_retry method."""

    @pytest.mark.unit
    def test_logs_retry(self, tmp_path: Path) -> None:
        """log_retry writes correct JSON entry."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_retry(
            attempt=2,
            max_attempts=3,
            status_code=429,
            wait_seconds=5.0,
        )

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["event"] == "retry"
        assert entry["level"] == "WARNING"
        assert entry["attempt"] == 2
        assert entry["max_attempts"] == 3
        assert entry["status"] == 429
        assert entry["wait_seconds"] == 5.0


class TestDebugLoggerLogCacheHitMiss:
    """Tests for DebugLogger cache hit/miss methods."""

    @pytest.mark.unit
    def test_logs_cache_hit(self, tmp_path: Path) -> None:
        """log_cache_hit writes correct JSON entry."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_cache_hit("contoso:users:list")

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["event"] == "cache_hit"
        assert entry["level"] == "DEBUG"
        assert entry["cache_key"] == "contoso:users:list"

    @pytest.mark.unit
    def test_logs_cache_miss(self, tmp_path: Path) -> None:
        """log_cache_miss writes correct JSON entry."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_cache_miss("contoso:users:list")

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["event"] == "cache_miss"
        assert entry["level"] == "DEBUG"
        assert entry["cache_key"] == "contoso:users:list"


class TestDebugLoggerLogError:
    """Tests for DebugLogger.log_error method."""

    @pytest.mark.unit
    def test_logs_error(self, tmp_path: Path) -> None:
        """log_error writes correct JSON entry."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_error(
            "Authentication failed",
            error_type="AuthenticationError",
            details="Token expired",
        )

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["event"] == "error"
        assert entry["level"] == "ERROR"
        assert entry["message"] == "Authentication failed"
        assert entry["error_type"] == "AuthenticationError"
        assert entry["details"] == "Token expired"

    @pytest.mark.unit
    def test_masks_secrets_in_error_message(self, tmp_path: Path) -> None:
        """log_error masks secrets in message."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_error(
            "Failed with token: Bearer secret123",
            details="Response contained Bearer anothersecret",
        )

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert "secret123" not in entry["message"]
        assert "[REDACTED]" in entry["message"]
        assert "anothersecret" not in entry["details"]
        assert "[REDACTED]" in entry["details"]


class TestDebugLoggerLogDebug:
    """Tests for DebugLogger.log_debug method."""

    @pytest.mark.unit
    def test_logs_debug_event(self, tmp_path: Path) -> None:
        """log_debug writes correct JSON entry."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_debug("custom_event", key1="value1", key2=42)

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert entry["event"] == "custom_event"
        assert entry["level"] == "DEBUG"
        assert entry["key1"] == "value1"
        assert entry["key2"] == 42

    @pytest.mark.unit
    def test_masks_secrets_in_debug_values(self, tmp_path: Path) -> None:
        """log_debug masks secrets in string values."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_debug("event", data="Bearer token123")

        content = log_path.read_text()
        entry = json.loads(content.strip())

        assert "token123" not in entry["data"]
        assert "[REDACTED]" in entry["data"]


class TestDebugLoggerMultipleEntries:
    """Tests for multiple log entries."""

    @pytest.mark.unit
    def test_multiple_entries_are_newline_delimited(self, tmp_path: Path) -> None:
        """Multiple log entries are on separate lines (JSON Lines format)."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_command_start("users list")
        logger.log_cache_miss("users:list")
        logger.log_command_complete(0, 100.0)

        content = log_path.read_text()
        lines = content.strip().split("\n")

        assert len(lines) == 3
        # Each line should be valid JSON
        for line in lines:
            entry = json.loads(line)
            assert "ts" in entry
            assert "correlation_id" in entry

    @pytest.mark.unit
    def test_entries_share_correlation_id(self, tmp_path: Path) -> None:
        """All entries from same logger share correlation ID."""
        log_path = tmp_path / "debug.log"
        logger = DebugLogger(log_path)

        logger.log_command_start("users list")
        logger.log_command_complete(0, 100.0)

        content = log_path.read_text()
        lines = content.strip().split("\n")

        entries = [json.loads(line) for line in lines]
        correlation_ids = {e["correlation_id"] for e in entries}

        assert len(correlation_ids) == 1  # All same correlation_id

    @pytest.mark.unit
    def test_appends_to_existing_file(self, tmp_path: Path) -> None:
        """DebugLogger appends to existing file."""
        log_path = tmp_path / "debug.log"

        # First logger
        logger1 = DebugLogger(log_path)
        logger1.log_command_start("command1")

        # Second logger (different correlation_id)
        logger2 = DebugLogger(log_path)
        logger2.log_command_start("command2")

        content = log_path.read_text()
        lines = content.strip().split("\n")

        assert len(lines) == 2
        entries = [json.loads(line) for line in lines]
        assert entries[0]["command"] == "command1"
        assert entries[1]["command"] == "command2"
        # Different correlation_ids
        assert entries[0]["correlation_id"] != entries[1]["correlation_id"]
