"""Comprehensive unit tests for DdbRepo."""

from datetime import UTC, datetime
from unittest.mock import Mock, patch

import botocore.exceptions
import pytest

from augint_billing_lib.adapters.ddb_repo import DdbRepo
from augint_billing_lib.models import Link


@pytest.mark.unit
class TestDdbRepoInit:
    """Test DdbRepo initialization."""

    def test_init(self):
        """Test DdbRepo initialization."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)
        assert repo.table == mock_table


@pytest.mark.unit
class TestDdbRepoMethods:
    """Test DdbRepo methods."""

    def test_coerce_with_valid_timestamp(self):
        """Test _coerce converts ISO timestamp to datetime."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        item = {
            "api_key_id": "key_123",
            "stripe_customer_id": "cus_456",
            "plan": "free",
            "last_reported_usage_ts": "2024-01-15T10:30:00Z",
        }

        link = repo._coerce(item)
        assert isinstance(link, Link)
        assert link.api_key_id == "key_123"
        assert link.stripe_customer_id == "cus_456"
        assert link.plan == "free"
        assert isinstance(link.last_reported_usage_ts, datetime)
        assert link.last_reported_usage_ts.year == 2024
        assert link.last_reported_usage_ts.month == 1
        assert link.last_reported_usage_ts.day == 15

    def test_coerce_with_invalid_timestamp(self):
        """Test _coerce handles invalid timestamp gracefully."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        item = {
            "api_key_id": "key_123",
            "stripe_customer_id": "cus_456",
            "plan": "free",
            "last_reported_usage_ts": "invalid_timestamp",
        }

        link = repo._coerce(item)
        assert isinstance(link, Link)
        assert link.last_reported_usage_ts is None

    def test_coerce_without_timestamp(self):
        """Test _coerce handles missing timestamp."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        item = {"api_key_id": "key_123", "stripe_customer_id": "cus_456", "plan": "free"}

        link = repo._coerce(item)
        assert isinstance(link, Link)
        assert link.last_reported_usage_ts is None

    def test_get_by_api_key_found(self):
        """Test get_by_api_key returns link when found."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        mock_response = {
            "Item": {
                "api_key_id": "key_123",
                "stripe_customer_id": "cus_456",
                "plan": "metered",
                "metered_subscription_item_id": "si_789",
            }
        }
        mock_table.get_item.return_value = mock_response

        link = repo.get_by_api_key("key_123")

        assert link.api_key_id == "key_123"
        assert link.stripe_customer_id == "cus_456"
        assert link.plan == "metered"
        assert link.metered_subscription_item_id == "si_789"
        mock_table.get_item.assert_called_once_with(Key={"api_key_id": "key_123"})

    def test_get_by_api_key_not_found(self):
        """Test get_by_api_key raises KeyError when not found."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        mock_response = {}  # No Item key
        mock_table.get_item.return_value = mock_response

        with pytest.raises(KeyError) as exc:
            repo.get_by_api_key("key_nonexistent")

        assert "key_nonexistent" in str(exc.value)
        mock_table.get_item.assert_called_once_with(Key={"api_key_id": "key_nonexistent"})

    def test_get_by_customer_multiple_links(self):
        """Test get_by_customer returns multiple links."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        mock_response = {
            "Items": [
                {"api_key_id": "key_1", "stripe_customer_id": "cus_456", "plan": "free"},
                {
                    "api_key_id": "key_2",
                    "stripe_customer_id": "cus_456",
                    "plan": "metered",
                    "metered_subscription_item_id": "si_123",
                },
            ]
        }
        mock_table.query.return_value = mock_response

        links = repo.get_by_customer("cus_456")

        assert len(links) == 2
        assert links[0].api_key_id == "key_1"
        assert links[0].plan == "free"
        assert links[1].api_key_id == "key_2"
        assert links[1].plan == "metered"

        mock_table.query.assert_called_once()
        call_args = mock_table.query.call_args
        assert call_args.kwargs["IndexName"] == "gsi_stripe_customer"

    def test_get_by_customer_no_links(self):
        """Test get_by_customer returns empty list when no links."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        mock_response = {"Items": []}
        mock_table.query.return_value = mock_response

        links = repo.get_by_customer("cus_no_links")

        assert links == []
        mock_table.query.assert_called_once()

    def test_save_with_datetime(self):
        """Test save converts datetime to ISO string."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        timestamp = datetime(2024, 1, 15, 10, 30, 0, tzinfo=UTC)
        link = Link(
            api_key_id="key_123",
            stripe_customer_id="cus_456",
            plan="metered",
            metered_subscription_item_id="si_789",
            last_reported_usage_ts=timestamp,
        )

        repo.save(link)

        mock_table.put_item.assert_called_once()
        item = mock_table.put_item.call_args.kwargs["Item"]
        assert item["api_key_id"] == "key_123"
        assert item["stripe_customer_id"] == "cus_456"
        assert item["plan"] == "metered"
        assert item["last_reported_usage_ts"] == "2024-01-15T10:30:00+00:00"

    def test_save_without_datetime(self):
        """Test save handles None timestamp."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        link = Link(api_key_id="key_123", stripe_customer_id="cus_456", plan="free")

        repo.save(link)

        mock_table.put_item.assert_called_once()
        item = mock_table.put_item.call_args.kwargs["Item"]
        assert item["api_key_id"] == "key_123"
        assert item["stripe_customer_id"] == "cus_456"
        assert item["plan"] == "free"
        assert item["last_reported_usage_ts"] is None

    def test_scan_metered_filters_correctly(self):
        """Test scan_metered returns only metered plans with subscription items."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        mock_response = {
            "Items": [
                {
                    "api_key_id": "key_1",
                    "stripe_customer_id": "cus_1",
                    "plan": "free",  # Should be filtered out
                },
                {
                    "api_key_id": "key_2",
                    "stripe_customer_id": "cus_2",
                    "plan": "metered",
                    "metered_subscription_item_id": "si_123",  # Should be included
                },
                {
                    "api_key_id": "key_3",
                    "stripe_customer_id": "cus_3",
                    "plan": "metered",  # No subscription item, should be filtered out
                },
                {
                    "api_key_id": "key_4",
                    "stripe_customer_id": "cus_4",
                    "plan": "metered",
                    "metered_subscription_item_id": "si_456",  # Should be included
                },
            ]
        }
        mock_table.scan.return_value = mock_response

        links = repo.scan_metered()

        assert len(links) == 2
        assert links[0].api_key_id == "key_2"
        assert links[0].metered_subscription_item_id == "si_123"
        assert links[1].api_key_id == "key_4"
        assert links[1].metered_subscription_item_id == "si_456"

        mock_table.scan.assert_called_once()

    def test_scan_metered_empty_table(self):
        """Test scan_metered returns empty list for empty table."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        mock_response = {"Items": []}
        mock_table.scan.return_value = mock_response

        links = repo.scan_metered()

        assert links == []
        mock_table.scan.assert_called_once()


@pytest.mark.unit
class TestDdbRepoRetry:
    """Test retry decorator on DdbRepo methods."""

    @patch("augint_billing_lib.adapters.ddb_repo.retry")
    def test_methods_have_retry_decorator(self, mock_retry):
        """Test that all public methods have retry decorator."""
        # The retry decorator is applied at the method level
        # We just need to verify it's imported and used
        from augint_billing_lib.adapters.ddb_repo import retry as actual_retry

        assert actual_retry is not None

    def test_get_by_api_key_retries_on_client_error(self):
        """Test get_by_api_key retries on ClientError."""
        mock_table = Mock()
        repo = DdbRepo(mock_table)

        # First call raises error, second succeeds
        mock_table.get_item.side_effect = [
            botocore.exceptions.ClientError(
                {"Error": {"Code": "ProvisionedThroughputExceededException"}}, "GetItem"
            ),
            {"Item": {"api_key_id": "key_123", "stripe_customer_id": "cus_456", "plan": "free"}},
        ]

        # This should succeed after retry
        link = repo.get_by_api_key("key_123")
        assert link.api_key_id == "key_123"
        assert mock_table.get_item.call_count == 2
