"""Integration tests for audit trail functionality."""

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

import pytest

from augint_billing_lib.models import Link, UsageReport
from augint_billing_lib.service import BillingService


@pytest.fixture
def mock_repo():
    """Create a mock customer repository."""
    return MagicMock()


@pytest.fixture
def mock_stripe():
    """Create a mock Stripe port."""
    stripe = MagicMock()
    # Mock report_usage to return a proper response
    stripe.report_usage.return_value = {"id": "ur_test123", "object": "usage_record"}
    return stripe


@pytest.fixture
def mock_usage():
    """Create a mock usage source."""
    return MagicMock()


@pytest.fixture
def mock_audit():
    """Create a mock audit port."""
    return MagicMock()


@pytest.fixture
def service_with_audit(mock_repo, mock_stripe, mock_usage, mock_audit):
    """Create a BillingService with audit enabled."""
    return BillingService(
        repo=mock_repo,
        stripe=mock_stripe,
        usage=mock_usage,
        audit=mock_audit,
    )


@pytest.fixture
def service_without_audit(mock_repo, mock_stripe, mock_usage):
    """Create a BillingService without audit."""
    return BillingService(
        repo=mock_repo,
        stripe=mock_stripe,
        usage=mock_usage,
        audit=None,
    )


@pytest.fixture
def sample_link():
    """Create a sample Link for testing."""
    return Link(
        api_key_id="key_123",
        stripe_customer_id="cus_abc",
        plan="metered",
        usage_plan_id="METERED",
        metered_subscription_item_id="si_xyz",
        last_usage_window_end=datetime(2024, 1, 15, 9, 0, 0, tzinfo=UTC),
    )


class TestAuditIntegration:
    """Test suite for audit trail integration."""

    @patch.dict("os.environ", {"AWS_REQUEST_ID": "req_123", "AWS_LAMBDA_FUNCTION_VERSION": "v1"})
    def test_audit_recorded_on_successful_usage_report(
        self, service_with_audit, mock_repo, mock_stripe, mock_usage, mock_audit, sample_link
    ):
        """Test that audit trail is recorded when usage is reported successfully."""
        # Setup
        mock_repo.scan_metered.return_value = [sample_link]
        mock_usage.get_usage.return_value = 1500

        since = datetime(2024, 1, 15, 9, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)

        # Execute
        reports = service_with_audit.reconcile_usage_window(since, until)

        # Verify usage was reported to Stripe
        mock_stripe.report_usage.assert_called_once()

        # Verify audit was recorded
        mock_audit.record_usage_report.assert_called_once()

        # Check the audit report contents
        audit_call = mock_audit.record_usage_report.call_args[0][0]
        assert isinstance(audit_call, UsageReport)
        assert audit_call.api_key_id == "key_123"
        assert audit_call.units_reported == 1500
        assert audit_call.stripe_customer_id == "cus_abc"
        assert audit_call.window_start == since
        assert audit_call.window_end == until

        # Verify repository was updated
        mock_repo.save.assert_called_once()

        # Verify result
        assert len(reports) == 1
        assert reports[0]["units"] == 1500

    def test_audit_not_recorded_when_disabled(
        self, service_without_audit, mock_repo, mock_stripe, mock_usage, sample_link
    ):
        """Test that audit is not recorded when audit port is None."""
        # Setup
        mock_repo.scan_metered.return_value = [sample_link]
        mock_usage.get_usage.return_value = 1500

        since = datetime(2024, 1, 15, 9, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)

        # Execute
        reports = service_without_audit.reconcile_usage_window(since, until)

        # Verify usage was reported
        mock_stripe.report_usage.assert_called_once()

        # Verify repository was updated
        mock_repo.save.assert_called_once()

        # Verify result
        assert len(reports) == 1

    @patch("augint_billing_lib.service.log_event")
    def test_audit_failure_does_not_stop_billing(
        self,
        mock_log_event,
        service_with_audit,
        mock_repo,
        mock_stripe,
        mock_usage,
        mock_audit,
        sample_link,
    ):
        """Test that audit failure doesn't stop the billing process."""
        # Setup
        mock_repo.scan_metered.return_value = [sample_link]
        mock_usage.get_usage.return_value = 1500

        # Make audit recording fail
        mock_audit.record_usage_report.side_effect = Exception("Audit DB unavailable")

        since = datetime(2024, 1, 15, 9, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)

        # Execute - should not raise despite audit failure
        reports = service_with_audit.reconcile_usage_window(since, until)

        # Verify usage was still reported to Stripe
        mock_stripe.report_usage.assert_called_once()

        # Verify audit was attempted
        mock_audit.record_usage_report.assert_called_once()

        # Verify error was logged
        error_logs = [
            call
            for call in mock_log_event.call_args_list
            if call[0][0] == "error" and call[0][1] == "audit_trail_failed"
        ]
        assert len(error_logs) == 1

        # Verify repository was still updated
        mock_repo.save.assert_called_once()

        # Verify result
        assert len(reports) == 1
        assert reports[0]["units"] == 1500

    def test_audit_captures_previous_window_end(
        self, service_with_audit, mock_repo, mock_stripe, mock_usage, mock_audit
    ):
        """Test that audit captures the previous window end for gap detection."""
        # Create link with previous window end
        link = Link(
            api_key_id="key_123",
            stripe_customer_id="cus_abc",
            plan="metered",
            usage_plan_id="METERED",
            metered_subscription_item_id="si_xyz",
            last_usage_window_end=datetime(2024, 1, 15, 8, 0, 0, tzinfo=UTC),
        )

        mock_repo.scan_metered.return_value = [link]
        mock_usage.get_usage.return_value = 2000

        # Report for a window that starts after last window end (gap exists)
        since = datetime(2024, 1, 15, 9, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)

        # Execute
        service_with_audit.reconcile_usage_window(since, until)

        # Check audit report captured previous window end
        audit_call = mock_audit.record_usage_report.call_args[0][0]
        assert audit_call.previous_window_end == datetime(2024, 1, 15, 8, 0, 0, tzinfo=UTC)

    def test_audit_records_zero_usage(
        self, service_with_audit, mock_repo, mock_stripe, mock_usage, mock_audit, sample_link
    ):
        """Test that zero usage is not reported to Stripe but link is still updated."""
        # Setup
        mock_repo.scan_metered.return_value = [sample_link]
        mock_usage.get_usage.return_value = 0  # Zero usage

        since = datetime(2024, 1, 15, 9, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)

        # Execute
        reports = service_with_audit.reconcile_usage_window(since, until)

        # Verify nothing was reported to Stripe (zero usage)
        mock_stripe.report_usage.assert_not_called()

        # Verify audit was NOT recorded (zero usage not reported)
        mock_audit.record_usage_report.assert_not_called()

        # Verify repository was NOT updated for zero usage
        mock_repo.save.assert_not_called()

        # Verify empty result
        assert len(reports) == 0

    def test_audit_records_multiple_customers(
        self, service_with_audit, mock_repo, mock_stripe, mock_usage, mock_audit
    ):
        """Test that audit records are created for multiple customers."""
        # Create multiple links
        links = [
            Link(
                api_key_id=f"key_{i}",
                stripe_customer_id=f"cus_{i}",
                plan="metered",
                usage_plan_id="METERED",
                metered_subscription_item_id=f"si_{i}",
            )
            for i in range(3)
        ]

        mock_repo.scan_metered.return_value = links
        mock_usage.get_usage.side_effect = [1000, 2000, 3000]  # Different usage for each

        since = datetime(2024, 1, 15, 9, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)

        # Execute
        reports = service_with_audit.reconcile_usage_window(since, until)

        # Verify all were reported to Stripe
        assert mock_stripe.report_usage.call_count == 3

        # Verify all were audited
        assert mock_audit.record_usage_report.call_count == 3

        # Verify each audit record has correct data
        audit_calls = mock_audit.record_usage_report.call_args_list
        for i, call in enumerate(audit_calls):
            report = call[0][0]
            assert report.api_key_id == f"key_{i}"
            assert report.stripe_customer_id == f"cus_{i}"
            assert report.units_reported == (i + 1) * 1000

        # Verify all were saved
        assert mock_repo.save.call_count == 3

        # Verify results
        assert len(reports) == 3

    @patch.dict(
        "os.environ", {"AWS_REQUEST_ID": "req_999", "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST"}
    )
    def test_audit_captures_environment_metadata(
        self, service_with_audit, mock_repo, mock_stripe, mock_usage, mock_audit, sample_link
    ):
        """Test that audit captures environment metadata correctly."""
        # Setup
        mock_repo.scan_metered.return_value = [sample_link]
        mock_usage.get_usage.return_value = 1500

        since = datetime(2024, 1, 15, 9, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)

        # Execute
        service_with_audit.reconcile_usage_window(since, until)

        # Check audit report metadata
        audit_call = mock_audit.record_usage_report.call_args[0][0]
        assert audit_call.reported_by_request_id == "req_999"
        assert audit_call.lambda_function_version == "$LATEST"
        assert audit_call.library_version == "2.6.1"  # From __version__

    def test_audit_with_overlapping_windows_prevented(
        self, service_with_audit, mock_repo, mock_stripe, mock_usage, mock_audit
    ):
        """Test that overlapping windows are prevented and not audited."""
        # Create link with last window end AFTER our 'until' time
        link = Link(
            api_key_id="key_123",
            stripe_customer_id="cus_abc",
            plan="metered",
            usage_plan_id="METERED",
            metered_subscription_item_id="si_xyz",
            last_usage_window_end=datetime(
                2024, 1, 15, 11, 0, 0, tzinfo=UTC
            ),  # Already processed future window
        )

        mock_repo.scan_metered.return_value = [link]

        since = datetime(2024, 1, 15, 9, 0, 0, tzinfo=UTC)
        until = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC)  # Trying to process earlier window

        # Execute
        reports = service_with_audit.reconcile_usage_window(since, until)

        # Verify nothing was reported (window already processed)
        mock_stripe.report_usage.assert_not_called()
        mock_audit.record_usage_report.assert_not_called()
        mock_repo.save.assert_not_called()

        # Verify empty result
        assert len(reports) == 0
