"""Property-based tests for usage window calculations."""

from datetime import UTC, datetime, timedelta
from unittest import mock

from hypothesis import HealthCheck, given, settings
from hypothesis import strategies as st

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


class TestUsageWindowProperties:
    """Property-based tests for usage window handling."""

    def test_no_overlap_simple(self):
        """Test that usage windows never overlap - simple case."""
        # Setup
        repo = mock.MagicMock()
        stripe = mock.MagicMock()
        usage = mock.MagicMock()
        service = BillingService(repo, stripe, usage=usage)

        # Create a link with a previous window end at 10:00
        previous_end = datetime(2024, 1, 15, 10, 0, tzinfo=UTC)

        link = Link(
            api_key_id="test_key",
            stripe_customer_id="cus_test",
            plan="METERED",
            usage_plan_id="metered_plan",
            metered_subscription_item_id="si_test",
            last_usage_window_end=previous_end,
        )

        repo.scan_metered.return_value = [link]
        usage.get_usage.return_value = 100
        stripe.report_usage.return_value = {"id": "ur_test"}

        # Try to report for window 09:00-11:00 (would overlap with previous end at 10:00)
        window_start = datetime(2024, 1, 15, 9, 0, tzinfo=UTC)
        window_end = datetime(2024, 1, 15, 11, 0, tzinfo=UTC)

        # Execute
        service.reconcile_usage_window(window_start, window_end)

        # Verify the actual window was adjusted to start at 10:00 (no overlap)
        usage_call_args = usage.get_usage.call_args[0]
        actual_start = usage_call_args[2]
        actual_end = usage_call_args[3]

        assert actual_start == previous_end, f"Expected start at {previous_end}, got {actual_start}"
        assert actual_end == window_end, f"Expected end at {window_end}, got {actual_end}"

        # Verify idempotency key uses adjusted window
        stripe_call_args = stripe.report_usage.call_args[0]
        idempotency_key = stripe_call_args[3]
        assert "2024-01-15T10:00:00+00:00" in idempotency_key  # Adjusted start
        assert "2024-01-15T11:00:00+00:00" in idempotency_key  # Original end

    @given(
        num_customers=st.integers(min_value=1, max_value=10),
        usage_amounts=st.lists(st.integers(min_value=0, max_value=10000), min_size=1, max_size=10),
    )
    @settings(suppress_health_check=[HealthCheck.too_slow])
    def test_all_customers_processed(self, num_customers, usage_amounts):
        """Test that all metered customers are processed in reconciliation."""
        # Ensure we have enough usage amounts for all customers
        usage_amounts = (usage_amounts * num_customers)[:num_customers]

        # Setup
        repo = mock.MagicMock()
        stripe = mock.MagicMock()
        usage = mock.MagicMock()
        service = BillingService(repo, stripe, usage=usage)

        # Create multiple customers
        links = []
        for i in range(num_customers):
            link = Link(
                api_key_id=f"key_{i}",
                stripe_customer_id=f"cus_{i}",
                plan="METERED",
                usage_plan_id="metered_plan",
                metered_subscription_item_id=f"si_{i}",
            )
            links.append(link)

        repo.scan_metered.return_value = links
        usage.get_usage.side_effect = usage_amounts
        stripe.report_usage.return_value = {"id": "ur_test"}

        # Execute
        now = datetime.now(UTC)
        one_hour_ago = now - timedelta(hours=1)
        result = service.reconcile_usage_window(one_hour_ago, now)

        # Verify all customers were queried for usage
        assert usage.get_usage.call_count == num_customers

        # Verify only customers with non-zero usage were reported to Stripe
        expected_reports = sum(1 for u in usage_amounts if u > 0)
        assert stripe.report_usage.call_count == expected_reports

        # Verify all customers with usage are in results
        assert len(result) == expected_reports

    @given(
        window_start_hour=st.integers(min_value=0, max_value=23),
        window_duration_hours=st.integers(min_value=1, max_value=72),
        base_date=st.dates(
            min_value=datetime(2020, 1, 1, tzinfo=UTC).date(),
            max_value=datetime(2030, 1, 1, tzinfo=UTC).date(),
        ),
    )
    def test_idempotency_key_uniqueness(self, window_start_hour, window_duration_hours, base_date):
        """Test that idempotency keys are unique for different time windows."""
        # Setup
        repo = mock.MagicMock()
        stripe = mock.MagicMock()
        usage = mock.MagicMock()
        service = BillingService(repo, stripe, usage=usage)

        link = Link(
            api_key_id="test_key",
            stripe_customer_id="cus_test",
            plan="METERED",
            usage_plan_id="metered_plan",
            metered_subscription_item_id="si_test",
        )

        repo.scan_metered.return_value = [link]
        usage.get_usage.return_value = 100  # Non-zero usage
        stripe.report_usage.return_value = {"id": "ur_test"}

        # Create window from the generated date and hour
        window_start = datetime(
            base_date.year, base_date.month, base_date.day, window_start_hour, 0, 0, tzinfo=UTC
        )
        window_end = window_start + timedelta(hours=window_duration_hours)

        # Execute
        service.reconcile_usage_window(window_start, window_end)

        # Verify idempotency key format
        stripe_call_args = stripe.report_usage.call_args[0]
        idempotency_key = stripe_call_args[3]

        # Key should be: api_key:start_iso:end_iso
        expected_parts = [
            link.api_key_id,
            window_start.isoformat(),
            window_end.isoformat(),
        ]
        for part in expected_parts:
            assert part in idempotency_key, f"Idempotency key missing {part}"

    @given(
        usage_value=st.integers(min_value=-1000, max_value=1_000_000),
    )
    def test_negative_and_zero_usage_handling(self, usage_value):
        """Test that negative and zero usage values are handled correctly."""
        # Setup
        repo = mock.MagicMock()
        stripe = mock.MagicMock()
        usage = mock.MagicMock()
        service = BillingService(repo, stripe, usage=usage)

        link = Link(
            api_key_id="test_key",
            stripe_customer_id="cus_test",
            plan="METERED",
            usage_plan_id="metered_plan",
            metered_subscription_item_id="si_test",
        )

        repo.scan_metered.return_value = [link]
        usage.get_usage.return_value = usage_value
        stripe.report_usage.return_value = {"id": "ur_test"}

        # Execute
        now = datetime.now(UTC)
        one_hour_ago = now - timedelta(hours=1)
        result = service.reconcile_usage_window(one_hour_ago, now)

        # Verify behavior based on usage value
        if usage_value > 0:
            # Positive usage should be reported
            assert stripe.report_usage.call_count == 1
            assert len(result) == 1
        else:
            # Zero or negative usage should not be reported
            assert stripe.report_usage.call_count == 0
            assert len(result) == 0

    @given(
        overlapping_hours=st.floats(min_value=0.1, max_value=23.9),
    )
    def test_overlapping_window_adjustment(self, overlapping_hours):
        """Test that overlapping windows are automatically adjusted."""
        # Setup
        repo = mock.MagicMock()
        stripe = mock.MagicMock()
        usage = mock.MagicMock()
        service = BillingService(repo, stripe, usage=usage)

        # Create a link with a previous window that would overlap
        base_time = datetime(2024, 1, 15, 12, 0, tzinfo=UTC)
        previous_end = base_time - timedelta(hours=overlapping_hours)

        link = Link(
            api_key_id="test_key",
            stripe_customer_id="cus_test",
            plan="METERED",
            usage_plan_id="metered_plan",
            metered_subscription_item_id="si_test",
            last_usage_window_end=previous_end,
        )

        repo.scan_metered.return_value = [link]
        usage.get_usage.return_value = 100
        stripe.report_usage.return_value = {"id": "ur_test"}

        # Try to create an overlapping window
        window_end = base_time
        window_start = base_time - timedelta(hours=24)  # Would overlap with previous_end

        # Execute
        service.reconcile_usage_window(window_start, window_end)

        # Verify the window was adjusted to avoid overlap
        usage_call = usage.get_usage.call_args[0]
        actual_window_start = usage_call[2]
        actual_window_end = usage_call[3]

        # The adjusted window should start at or after the previous end
        assert actual_window_start >= previous_end, "Window was not adjusted to avoid overlap"
        assert actual_window_end == window_end, "Window end should not change"

        # Verify the idempotency key uses the adjusted window
        if stripe.report_usage.called:
            stripe_call = stripe.report_usage.call_args[0]
            idempotency_key = stripe_call[3]
            assert actual_window_start.isoformat() in idempotency_key
