"""Tests for usage event sinks."""

from datetime import UTC, datetime

import pytest

from noteflow.application.observability.ports import NullUsageEventSink, UsageEvent, UsageMetrics
from noteflow.infrastructure.observability.usage import (
    LoggingUsageEventSink,
    OtelUsageEventSink,
    create_usage_event_sink,
)

# Test constants
SAMPLE_TOKENS_INPUT = 1000
SAMPLE_TOKENS_OUTPUT = 500
SAMPLE_LATENCY_MS = 1234.5
SAMPLE_LATENCY_LOGGING = 500.0
SAMPLE_LATENCY_SIMPLE = 2000.0
SAMPLE_TOKENS_SIMPLE = 100


class TestUsageEvent:
    """Tests for UsageEvent dataclass."""

    def test_usage_event_has_timestamp(self) -> None:
        """UsageEvent automatically timestamps on creation."""
        before = datetime.now(tz=UTC)
        event = UsageEvent(event_type="test.event")
        after = datetime.now(tz=UTC)

        assert before <= event.timestamp <= after, "timestamp should be between before and after"

    def test_usage_event_stores_all_fields(self) -> None:
        """UsageEvent stores all provided fields."""
        expected_attributes: dict[str, object] = {"custom": "value"}
        event = UsageEvent(
            event_type="summarization.completed",
            meeting_id="meeting-123",
            workspace_id="workspace-456",
            project_id="project-789",
            provider_name="openai",
            model_name="gpt-4",
            tokens_input=SAMPLE_TOKENS_INPUT,
            tokens_output=SAMPLE_TOKENS_OUTPUT,
            latency_ms=SAMPLE_LATENCY_MS,
            success=True,
            attributes=expected_attributes,
        )

        assert (
            event.event_type == "summarization.completed"
        ), "event_type should match"
        assert event.meeting_id == "meeting-123", "meeting_id should match"
        assert event.workspace_id == "workspace-456", "workspace_id should match"
        assert event.project_id == "project-789", "project_id should match"
        assert event.provider_name == "openai", "provider_name should match"
        assert event.model_name == "gpt-4", "model_name should match"
        assert (
            event.tokens_input == SAMPLE_TOKENS_INPUT
        ), f"tokens_input should be {SAMPLE_TOKENS_INPUT}"
        assert (
            event.tokens_output == SAMPLE_TOKENS_OUTPUT
        ), f"tokens_output should be {SAMPLE_TOKENS_OUTPUT}"
        assert (
            event.latency_ms == SAMPLE_LATENCY_MS
        ), f"latency_ms should be {SAMPLE_LATENCY_MS}"
        assert event.success is True, "success should be True"
        assert event.attributes == expected_attributes, "attributes should match"

    def test_usage_event_defaults_to_success(self) -> None:
        """UsageEvent defaults to success=True."""
        event = UsageEvent(event_type="test")
        assert event.success is True, "success should default to True"

    def test_usage_event_supports_error_code(self) -> None:
        """UsageEvent supports error_code for failures."""
        event = UsageEvent(
            event_type="summarization.failed",
            success=False,
            error_code="provider_error",
        )
        assert event.success is False, "success should be False for failures"
        assert (
            event.error_code == "provider_error"
        ), "error_code should match provided value"

    def test_usage_event_is_immutable(self) -> None:
        """UsageEvent is frozen (immutable)."""
        from dataclasses import FrozenInstanceError

        event = UsageEvent(event_type="test")
        with pytest.raises(FrozenInstanceError, match="cannot assign"):
            event.event_type = "modified"


class TestNullUsageEventSink:
    """Tests for NullUsageEventSink."""

    def test_null_sink_accepts_events(self) -> None:
        """NullUsageEventSink accepts events without error."""
        sink = NullUsageEventSink()
        event = UsageEvent(event_type="test.event")
        sink.record(event)  # Should not raise

    def test_null_sink_accepts_simple_events(self) -> None:
        """NullUsageEventSink accepts simple events without error."""
        sink = NullUsageEventSink()
        metrics = UsageMetrics(provider_name="test", tokens_input=SAMPLE_TOKENS_SIMPLE)
        sink.record_simple(
            "test.event",
            metrics,
            meeting_id="123",
        )  # Should not raise


class TestLoggingUsageEventSink:
    """Tests for LoggingUsageEventSink."""

    def test_logging_sink_logs_events(self, caplog: pytest.LogCaptureFixture) -> None:
        """LoggingUsageEventSink logs usage events."""
        sink = LoggingUsageEventSink()
        event = UsageEvent(
            event_type="summarization.completed",
            meeting_id="meeting-123",
            provider_name="openai",
            model_name="gpt-4",
            tokens_input=SAMPLE_TOKENS_INPUT,
            latency_ms=SAMPLE_LATENCY_LOGGING,
        )

        with caplog.at_level("INFO", logger="noteflow.usage"):
            sink.record(event)

        assert (
            "summarization.completed" in caplog.text
        ), "log should contain event_type"
        assert "meeting-123" in caplog.text, "log should contain meeting_id"
        assert "openai" in caplog.text, "log should contain provider_name"

    def test_logging_sink_simple_creates_event(
        self, caplog: pytest.LogCaptureFixture
    ) -> None:
        """LoggingUsageEventSink.record_simple creates and logs event."""
        sink = LoggingUsageEventSink()
        metrics = UsageMetrics(provider_name="whisper", latency_ms=SAMPLE_LATENCY_SIMPLE)

        with caplog.at_level("INFO", logger="noteflow.usage"):
            sink.record_simple(
                "transcription.completed",
                metrics,
                meeting_id="meeting-456",
            )

        assert (
            "transcription.completed" in caplog.text
        ), "log should contain event_type"
        assert "meeting-456" in caplog.text, "log should contain meeting_id"


class TestCreateUsageEventSink:
    """Tests for create_usage_event_sink factory."""

    def test_creates_otel_sink_when_otel_available(self) -> None:
        """Factory returns OtelUsageEventSink when OTel is available."""
        sink = create_usage_event_sink(use_otel=True, fallback_to_logging=True)
        assert isinstance(
            sink, OtelUsageEventSink
        ), "should return OtelUsageEventSink when OTel available"

    def test_creates_logging_sink_when_otel_unavailable(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Factory returns LoggingUsageEventSink when OTel unavailable."""
        monkeypatch.setattr(
            "noteflow.infrastructure.observability.usage.check_otel_available",
            lambda: False,
        )
        sink = create_usage_event_sink(use_otel=True, fallback_to_logging=True)
        assert isinstance(
            sink, LoggingUsageEventSink
        ), "should fall back to LoggingUsageEventSink when OTel unavailable"

    def test_creates_null_sink_when_logging_disabled(self) -> None:
        """Factory returns NullUsageEventSink when logging disabled."""
        sink = create_usage_event_sink(use_otel=False, fallback_to_logging=False)
        assert isinstance(
            sink, NullUsageEventSink
        ), "should return NullUsageEventSink when logging disabled"

    def test_creates_logging_sink_when_otel_disabled(self) -> None:
        """Factory returns LoggingUsageEventSink when OTel disabled."""
        sink = create_usage_event_sink(use_otel=False, fallback_to_logging=True)
        assert isinstance(
            sink, LoggingUsageEventSink
        ), "should return LoggingUsageEventSink when OTel disabled"
