"""Unit tests for DdbAuditRepo."""

from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock, patch

import pytest
from botocore.exceptions import ClientError

from augint_billing_lib.adapters.ddb_audit import DdbAuditRepo
from augint_billing_lib.models import UsageReport


@pytest.fixture
def mock_table():
    """Create a mock DynamoDB table."""
    return MagicMock()


@pytest.fixture
def audit_repo(mock_table):
    """Create a DdbAuditRepo with mocked dependencies."""
    with patch("augint_billing_lib.adapters.ddb_audit.boto3.resource") as mock_resource:
        mock_resource.return_value.Table.return_value = mock_table
        with patch.dict("os.environ", {"STACK_NAME": "test-stack"}):
            return DdbAuditRepo()


@pytest.fixture
def sample_report():
    """Create a sample UsageReport for testing."""
    now = datetime.now(UTC)
    return UsageReport(
        api_key_id="key_123",
        window_start=now - timedelta(hours=1),
        window_end=now,
        units_reported=1500,
        stripe_customer_id="cus_abc",
        stripe_subscription_item_id="si_xyz",
        usage_plan_id="METERED",
        stripe_idempotency_key="test_key",
        stripe_response={"status": "success"},
        stripe_usage_record_id="ur_123",
        reported_at=now,
        reported_by_request_id="req_123",
        lambda_function_version="v1",
        library_version="2.6.1",
        source_usage_count=1500,
        previous_window_end=now - timedelta(hours=2),
    )


class TestDdbAuditRepo:
    """Test suite for DdbAuditRepo."""

    def test_init_with_table_name(self):
        """Test initialization with explicit table name."""
        with patch("augint_billing_lib.adapters.ddb_audit.boto3.resource"):
            repo = DdbAuditRepo(table_name="custom-table")
            assert repo.table_name == "custom-table"

    def test_init_from_environment(self):
        """Test initialization from environment variables."""
        with (
            patch("augint_billing_lib.adapters.ddb_audit.boto3.resource"),
            patch.dict("os.environ", {"STACK_NAME": "my-stack"}),
        ):
            repo = DdbAuditRepo()
            assert repo.table_name == "my-stack-usage-audit"

    def test_init_missing_stack_name(self):
        """Test initialization fails without STACK_NAME."""
        with (
            patch("augint_billing_lib.adapters.ddb_audit.boto3.resource"),
            patch.dict("os.environ", {}, clear=True),
            pytest.raises(ValueError, match="STACK_NAME environment variable not set"),
        ):
            DdbAuditRepo()

    def test_record_usage_report_success(self, audit_repo, mock_table, sample_report):
        """Test successfully recording a usage report."""
        mock_table.put_item.return_value = {"ResponseMetadata": {"HTTPStatusCode": 200}}

        audit_repo.record_usage_report(sample_report)

        # Verify put_item was called with correct parameters
        mock_table.put_item.assert_called_once()
        call_args = mock_table.put_item.call_args
        assert "Item" in call_args.kwargs
        assert "ConditionExpression" in call_args.kwargs

        # Verify the item contains expected fields
        item = call_args.kwargs["Item"]
        assert item["api_key_id"] == "key_123"
        assert item["units_reported"] == 1500

    def test_record_usage_report_idempotent(self, audit_repo, mock_table, sample_report):
        """Test that recording the same report twice is idempotent."""
        # First call succeeds, second call gets ConditionalCheckFailed
        error = ClientError({"Error": {"Code": "ConditionalCheckFailedException"}}, "PutItem")
        mock_table.put_item.side_effect = [
            {"ResponseMetadata": {"HTTPStatusCode": 200}},
            error,
        ]

        # First record should succeed
        audit_repo.record_usage_report(sample_report)

        # Second record should not raise (idempotent)
        audit_repo.record_usage_report(sample_report)

        # Verify put_item was called twice
        assert mock_table.put_item.call_count == 2

    def test_record_usage_report_error(self, audit_repo, mock_table, sample_report):
        """Test handling of DynamoDB errors."""
        error = ClientError({"Error": {"Code": "InternalServerError"}}, "PutItem")
        mock_table.put_item.side_effect = error

        with pytest.raises(ClientError):
            audit_repo.record_usage_report(sample_report)

    def test_get_reports_for_window(self, audit_repo, mock_table):
        """Test retrieving reports for a specific window."""
        # Mock query response
        mock_table.query.return_value = {
            "Items": [
                {
                    "api_key_id": "key_123",
                    "window_start_iso": "2024-01-15T10:00:00+00:00",
                    "window_end_iso": "2024-01-15T11:00:00+00:00",
                    "units_reported": 1500,
                    "stripe_customer_id": "cus_abc",
                    "stripe_subscription_item_id": "si_xyz",
                    "usage_plan_id": "METERED",
                    "stripe_idempotency_key": "test_key",
                    "stripe_response": {},
                    "stripe_usage_record_id": "ur_123",
                    "reported_at_iso": "2024-01-15T11:05:00+00:00",
                    "reported_by": "req_123",
                    "lambda_function_version": "v1",
                    "library_version": "2.6.1",
                }
            ]
        }

        start = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)
        end = datetime(2024, 1, 15, 11, 0, 0, tzinfo=UTC)

        reports = audit_repo.get_reports_for_window("key_123", start, end)

        assert len(reports) == 1
        assert reports[0].api_key_id == "key_123"
        assert reports[0].units_reported == 1500

        # Verify query was called with correct parameters
        mock_table.query.assert_called_once()

    def test_get_reports_for_window_with_pagination(self, audit_repo, mock_table):
        """Test handling pagination in window queries."""
        # Mock paginated responses
        mock_table.query.side_effect = [
            {
                "Items": [
                    {
                        "api_key_id": "key_123",
                        "window_start_iso": "2024-01-15T10:00:00+00:00",
                        "window_end_iso": "2024-01-15T11:00:00+00:00",
                        "units_reported": 1000,
                        "stripe_customer_id": "cus_abc",
                        "stripe_subscription_item_id": "si_xyz",
                        "usage_plan_id": "METERED",
                        "stripe_idempotency_key": "key1",
                        "stripe_response": {},
                        "reported_at_iso": "2024-01-15T11:05:00+00:00",
                        "reported_by": "req_123",
                        "lambda_function_version": "v1",
                        "library_version": "2.6.1",
                    }
                ],
                "LastEvaluatedKey": {"api_key_id": "key_123", "window_end_timestamp": 1705316400},
            },
            {
                "Items": [
                    {
                        "api_key_id": "key_123",
                        "window_start_iso": "2024-01-15T11:00:00+00:00",
                        "window_end_iso": "2024-01-15T12:00:00+00:00",
                        "units_reported": 1500,
                        "stripe_customer_id": "cus_abc",
                        "stripe_subscription_item_id": "si_xyz",
                        "usage_plan_id": "METERED",
                        "stripe_idempotency_key": "key2",
                        "stripe_response": {},
                        "reported_at_iso": "2024-01-15T12:05:00+00:00",
                        "reported_by": "req_124",
                        "lambda_function_version": "v1",
                        "library_version": "2.6.1",
                    }
                ]
            },
        ]

        start = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)
        end = datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC)

        reports = audit_repo.get_reports_for_window("key_123", start, end)

        assert len(reports) == 2
        assert reports[0].units_reported == 1000
        assert reports[1].units_reported == 1500

        # Verify query was called twice (pagination)
        assert mock_table.query.call_count == 2

    def test_get_reports_for_customer(self, audit_repo, mock_table):
        """Test retrieving reports for a customer."""
        mock_table.query.return_value = {
            "Items": [
                {
                    "api_key_id": "key_123",
                    "window_start_iso": "2024-01-15T10:00:00+00:00",
                    "window_end_iso": "2024-01-15T11:00:00+00:00",
                    "units_reported": 1500,
                    "stripe_customer_id": "cus_abc",
                    "stripe_subscription_item_id": "si_xyz",
                    "usage_plan_id": "METERED",
                    "stripe_idempotency_key": "test_key",
                    "stripe_response": {},
                    "reported_at_iso": "2024-01-15T11:05:00+00:00",
                    "reported_by": "req_123",
                    "lambda_function_version": "v1",
                    "library_version": "2.6.1",
                }
            ]
        }

        since = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 31, 23, 59, 59, tzinfo=UTC)

        reports = audit_repo.get_reports_for_customer("cus_abc", since, until)

        assert len(reports) == 1
        assert reports[0].stripe_customer_id == "cus_abc"

        # Verify query used the GSI
        call_args = mock_table.query.call_args
        assert call_args.kwargs["IndexName"] == "gsi_stripe_customer"

    def test_find_gaps_no_gaps(self, audit_repo, mock_table):
        """Test gap detection when no gaps exist."""
        # Create consecutive hourly reports
        base_time = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)
        items = []
        for i in range(5):
            start = base_time + timedelta(hours=i)
            end = base_time + timedelta(hours=i + 1)
            items.append(
                {
                    "api_key_id": "key_123",
                    "window_start_iso": start.isoformat(),
                    "window_end_iso": end.isoformat(),
                    "units_reported": 1000 + i * 100,
                    "stripe_customer_id": "cus_abc",
                    "stripe_subscription_item_id": "si_xyz",
                    "usage_plan_id": "METERED",
                    "stripe_idempotency_key": f"key_{i}",
                    "stripe_response": {},
                    "reported_at_iso": end.isoformat(),
                    "reported_by": f"req_{i}",
                    "lambda_function_version": "v1",
                    "library_version": "2.6.1",
                }
            )

        mock_table.query.return_value = {"Items": items}

        gaps = audit_repo.find_gaps("key_123", timedelta(hours=1))

        assert len(gaps) == 0

    def test_find_gaps_with_gaps(self, audit_repo, mock_table):
        """Test gap detection when gaps exist."""
        base_time = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)

        # Create reports with a 2-hour gap (missing 11:00-12:00 and 12:00-13:00)
        items = [
            {
                "api_key_id": "key_123",
                "window_start_iso": (base_time).isoformat(),
                "window_end_iso": (base_time + timedelta(hours=1)).isoformat(),
                "units_reported": 1000,
                "stripe_customer_id": "cus_abc",
                "stripe_subscription_item_id": "si_xyz",
                "usage_plan_id": "METERED",
                "stripe_idempotency_key": "key_0",
                "stripe_response": {},
                "reported_at_iso": (base_time + timedelta(hours=1)).isoformat(),
                "reported_by": "req_0",
                "lambda_function_version": "v1",
                "library_version": "2.6.1",
            },
            {
                "api_key_id": "key_123",
                "window_start_iso": (base_time + timedelta(hours=3)).isoformat(),
                "window_end_iso": (base_time + timedelta(hours=4)).isoformat(),
                "units_reported": 1200,
                "stripe_customer_id": "cus_abc",
                "stripe_subscription_item_id": "si_xyz",
                "usage_plan_id": "METERED",
                "stripe_idempotency_key": "key_3",
                "stripe_response": {},
                "reported_at_iso": (base_time + timedelta(hours=4)).isoformat(),
                "reported_by": "req_3",
                "lambda_function_version": "v1",
                "library_version": "2.6.1",
            },
        ]

        mock_table.query.return_value = {"Items": items}

        gaps = audit_repo.find_gaps("key_123", timedelta(hours=1))

        assert len(gaps) == 1
        gap_start, gap_end = gaps[0]
        assert gap_start == base_time + timedelta(hours=1)
        assert gap_end == base_time + timedelta(hours=3)

    def test_find_gaps_insufficient_data(self, audit_repo, mock_table):
        """Test gap detection with insufficient data."""
        # Only one report - can't detect gaps
        mock_table.query.return_value = {
            "Items": [
                {
                    "api_key_id": "key_123",
                    "window_start_iso": "2024-01-15T10:00:00+00:00",
                    "window_end_iso": "2024-01-15T11:00:00+00:00",
                    "units_reported": 1000,
                    "stripe_customer_id": "cus_abc",
                    "stripe_subscription_item_id": "si_xyz",
                    "usage_plan_id": "METERED",
                    "stripe_idempotency_key": "key_0",
                    "stripe_response": {},
                    "reported_at_iso": "2024-01-15T11:05:00+00:00",
                    "reported_by": "req_0",
                    "lambda_function_version": "v1",
                    "library_version": "2.6.1",
                }
            ]
        }

        gaps = audit_repo.find_gaps("key_123", timedelta(hours=1))

        assert len(gaps) == 0
