"""Tests for billing subscriptions endpoints."""

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

import pytest
from fastapi import HTTPException

from robosystems.models.api.billing.subscription import CancelSubscriptionRequest
from robosystems.models.core import User
from robosystems.models.core.billing import BillingSubscription
from robosystems.routers.billing.subscriptions import (
  cancel_subscription,
  get_subscription,
  list_subscriptions,
)


class TestListSubscriptions:
  """Tests for list_subscriptions endpoint."""

  @pytest.fixture
  def mock_user(self):
    user = Mock(spec=User)
    user.id = "user_123"
    return user

  @pytest.fixture
  def mock_db(self):
    db = Mock()
    return db

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_list_subscriptions_empty(self, mock_get_org_user, mock_user, mock_db):
    """Test listing subscriptions when org has none."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    mock_query = mock_db.query.return_value
    mock_query.filter.return_value.order_by.return_value.all.return_value = []

    result = await list_subscriptions("org_123", mock_user, mock_db, None)

    assert result == []

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_list_subscriptions_success(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Test successful subscription listing."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    mock_sub1 = Mock(spec=BillingSubscription)
    mock_sub1.id = "sub_1"
    mock_sub1.resource_type = "graph"
    mock_sub1.resource_id = "kg_123"
    mock_sub1.plan_name = "standard"
    mock_sub1.billing_interval = "monthly"
    mock_sub1.status = "active"
    mock_sub1.base_price_cents = 2999
    mock_sub1.current_period_start = datetime(2025, 1, 1)
    mock_sub1.current_period_end = datetime(2025, 2, 1)
    mock_sub1.started_at = datetime(2025, 1, 1)
    mock_sub1.canceled_at = None
    mock_sub1.ends_at = None
    mock_sub1.created_at = datetime(2025, 1, 1)

    mock_sub2 = Mock(spec=BillingSubscription)
    mock_sub2.id = "sub_2"
    mock_sub2.resource_type = "repository"
    mock_sub2.resource_id = "sec"
    mock_sub2.plan_name = "starter"
    mock_sub2.billing_interval = "monthly"
    mock_sub2.status = "canceled"
    mock_sub2.base_price_cents = 999
    mock_sub2.current_period_start = None
    mock_sub2.current_period_end = None
    mock_sub2.started_at = datetime(2024, 12, 1)
    mock_sub2.canceled_at = datetime(2025, 1, 15)
    mock_sub2.ends_at = datetime(2025, 2, 1)
    mock_sub2.created_at = datetime(2024, 12, 1)

    mock_query = mock_db.query.return_value
    mock_query.filter.return_value.order_by.return_value.all.return_value = [
      mock_sub1,
      mock_sub2,
    ]

    result = await list_subscriptions("org_123", mock_user, mock_db, None)

    assert len(result) == 2
    assert result[0].id == "sub_1"
    assert result[0].resource_type == "graph"
    assert result[0].resource_id == "kg_123"
    assert result[0].plan_name == "standard"
    assert result[0].status == "active"
    assert result[0].base_price_cents == 2999

    assert result[1].id == "sub_2"
    assert result[1].resource_type == "repository"
    assert result[1].status == "canceled"
    assert result[1].canceled_at is not None

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_list_subscriptions_error_handling(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Test error handling in subscription listing."""
    mock_get_org_user.side_effect = Exception("Database error")

    with pytest.raises(HTTPException) as exc:
      await list_subscriptions("org_123", mock_user, mock_db, None)

    assert exc.value.status_code == 500
    assert "Failed to retrieve subscriptions" in exc.value.detail

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_list_subscriptions_requires_membership(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Non-members should receive 403."""
    mock_get_org_user.return_value = None

    with pytest.raises(HTTPException) as exc:
      await list_subscriptions("org_123", mock_user, mock_db, None)

    assert exc.value.status_code == 403
    assert exc.value.detail == "You are not a member of this organization"


class TestGetSubscription:
  """Tests for get_subscription endpoint."""

  @pytest.fixture
  def mock_user(self):
    user = Mock(spec=User)
    user.id = "user_123"
    return user

  @pytest.fixture
  def mock_db(self):
    return Mock()

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_get_subscription_success(self, mock_get_org_user, mock_user, mock_db):
    """Test successful subscription retrieval."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    mock_subscription = Mock(spec=BillingSubscription)
    mock_subscription.id = "sub_123"
    mock_subscription.resource_type = "graph"
    mock_subscription.resource_id = "kg_456"
    mock_subscription.plan_name = "enterprise"
    mock_subscription.billing_interval = "monthly"
    mock_subscription.status = "active"
    mock_subscription.base_price_cents = 9999
    mock_subscription.current_period_start = datetime(2025, 1, 1)
    mock_subscription.current_period_end = datetime(2025, 2, 1)
    mock_subscription.started_at = datetime(2025, 1, 1)
    mock_subscription.canceled_at = None
    mock_subscription.ends_at = None
    mock_subscription.created_at = datetime(2025, 1, 1)

    mock_query = mock_db.query.return_value
    mock_query.filter.return_value.first.return_value = mock_subscription

    result = await get_subscription("org_123", "sub_123", mock_user, mock_db, None)

    assert result.id == "sub_123"
    assert result.resource_type == "graph"
    assert result.resource_id == "kg_456"
    assert result.plan_name == "enterprise"
    assert result.status == "active"

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_get_subscription_not_found(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Test getting subscription that doesn't exist."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    mock_query = mock_db.query.return_value
    mock_query.filter.return_value.first.return_value = None

    with pytest.raises(HTTPException) as exc:
      await get_subscription("org_123", "sub_999", mock_user, mock_db, None)

    assert exc.value.status_code == 404
    assert "Subscription not found" in exc.value.detail

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_get_subscription_null_resource_id(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Test getting subscription with null resource_id."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    mock_subscription = Mock(spec=BillingSubscription)
    mock_subscription.id = "sub_123"
    mock_subscription.resource_type = "graph"
    mock_subscription.resource_id = None
    mock_subscription.plan_name = "standard"
    mock_subscription.billing_interval = "monthly"
    mock_subscription.status = "pending_provisioning"
    mock_subscription.base_price_cents = 2999
    mock_subscription.current_period_start = None
    mock_subscription.current_period_end = None
    mock_subscription.started_at = None
    mock_subscription.canceled_at = None
    mock_subscription.ends_at = None
    mock_subscription.created_at = datetime(2025, 1, 1)

    mock_query = mock_db.query.return_value
    mock_query.filter.return_value.first.return_value = mock_subscription

    result = await get_subscription("org_123", "sub_123", mock_user, mock_db, None)

    assert result.resource_id == ""
    assert result.status == "pending_provisioning"

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_get_subscription_error_handling(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Test error handling in subscription retrieval."""
    mock_get_org_user.side_effect = Exception("Database error")

    with pytest.raises(HTTPException) as exc:
      await get_subscription("org_123", "sub_123", mock_user, mock_db, None)

    assert exc.value.status_code == 500
    assert "Failed to retrieve subscription" in exc.value.detail

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_get_subscription_requires_membership(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Users outside the org should be blocked."""
    mock_get_org_user.return_value = None

    with pytest.raises(HTTPException) as exc:
      await get_subscription("org_123", "sub_1", mock_user, mock_db, None)

    assert exc.value.status_code == 403
    assert exc.value.detail == "You are not a member of this organization"


class TestCancelSubscription:
  """Tests for cancel_subscription endpoint."""

  @pytest.fixture
  def mock_user(self):
    user = Mock(spec=User)
    user.id = "user_123"
    return user

  @pytest.fixture
  def mock_db(self):
    return Mock()

  def _build_active_subscription(
    self, resource_type: str = "graph", resource_id: str = "kg_456"
  ) -> Mock:
    sub = Mock(spec=BillingSubscription)
    sub.id = "sub_123"
    sub.resource_type = resource_type
    sub.resource_id = resource_id
    sub.plan_name = "standard"
    sub.billing_interval = "monthly"
    sub.status = "active"
    sub.base_price_cents = 2999
    sub.current_period_start = datetime(2025, 1, 1)
    sub.current_period_end = datetime(2025, 2, 1)
    sub.started_at = datetime(2025, 1, 1)
    sub.canceled_at = None
    sub.ends_at = None
    sub.stripe_subscription_id = None
    sub.created_at = datetime(2025, 1, 1)
    return sub

  @pytest.mark.asyncio
  @patch("robosystems.routers.billing.subscriptions.BillingAuditLog")
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_period_end_repository_rejected(
    self, mock_get_org_user, _mock_audit, mock_user, mock_db
  ):
    """Period-end cancel of a repository sub is rejected — repo cancellation
    lives in the graph-scoped subscription router."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    subscription = self._build_active_subscription(
      resource_type="repository", resource_id="sec"
    )
    mock_db.query.return_value.filter.return_value.first.return_value = subscription

    with pytest.raises(HTTPException) as exc:
      await cancel_subscription(
        org_id="org_123",
        subscription_id="sub_123",
        body=CancelSubscriptionRequest(),
        current_user=mock_user,
        db=mock_db,
        _rate_limit=None,
      )

    assert exc.value.status_code == 400
    assert "/v1/graphs/sec/subscriptions/cancel" in exc.value.detail
    subscription.cancel.assert_not_called()

  @pytest.mark.asyncio
  @patch("robosystems.routers.billing.subscriptions.BillingAuditLog")
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_period_end_graph_rejected(
    self, mock_get_org_user, _mock_audit, mock_user, mock_db
  ):
    """Period-end cancel of a graph sub is rejected — graph cancellation
    (immediate or period-end) lives in graph ops, not billing."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    subscription = self._build_active_subscription(
      resource_type="graph", resource_id="kg_456"
    )
    mock_db.query.return_value.filter.return_value.first.return_value = subscription

    with pytest.raises(HTTPException) as exc:
      await cancel_subscription(
        org_id="org_123",
        subscription_id="sub_123",
        body=CancelSubscriptionRequest(),  # period-end (default)
        current_user=mock_user,
        db=mock_db,
        _rate_limit=None,
      )

    assert exc.value.status_code == 400
    assert "delete-graph" in exc.value.detail
    assert "kg_456" in exc.value.detail
    subscription.cancel.assert_not_called()

  @pytest.mark.asyncio
  @patch("robosystems.routers.billing.subscriptions.BillingAuditLog")
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_immediate_repository_rejected(
    self, mock_get_org_user, _mock_audit, mock_user, mock_db
  ):
    """Immediate cancel of a repository sub is rejected — repo cancellation
    (immediate or period-end) lives in the graph-scoped subscription router."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    subscription = self._build_active_subscription(
      resource_type="repository", resource_id="sec"
    )
    mock_db.query.return_value.filter.return_value.first.return_value = subscription

    with pytest.raises(HTTPException) as exc:
      await cancel_subscription(
        org_id="org_123",
        subscription_id="sub_123",
        body=CancelSubscriptionRequest(immediate=True, confirm="sec"),
        current_user=mock_user,
        db=mock_db,
        _rate_limit=None,
      )

    assert exc.value.status_code == 400
    assert "/v1/graphs/sec/subscriptions/cancel" in exc.value.detail
    subscription.cancel.assert_not_called()

  @pytest.mark.asyncio
  @patch("robosystems.routers.billing.subscriptions.BillingAuditLog")
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_immediate_graph_rejected(
    self, mock_get_org_user, _mock_audit, mock_user, mock_db
  ):
    """Immediate cancel of a graph sub is rejected — same redirect as the
    period-end path; graph cancellation belongs in graph ops."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    subscription = self._build_active_subscription(
      resource_type="graph", resource_id="kg_456"
    )
    mock_db.query.return_value.filter.return_value.first.return_value = subscription

    with pytest.raises(HTTPException) as exc:
      await cancel_subscription(
        org_id="org_123",
        subscription_id="sub_123",
        body=CancelSubscriptionRequest(immediate=True, confirm="kg_456"),
        current_user=mock_user,
        db=mock_db,
        _rate_limit=None,
      )

    assert exc.value.status_code == 400
    assert "delete-graph" in exc.value.detail
    assert "kg_456" in exc.value.detail
    subscription.cancel.assert_not_called()

  @pytest.mark.asyncio
  @patch("robosystems.routers.billing.subscriptions.BillingAuditLog")
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_immediate_missing_confirm(
    self, mock_get_org_user, _mock_audit, mock_user, mock_db
  ):
    """Immediate cancel of a non-resource-scoped sub without confirm → 400.

    Uses resource_type='addon' as a stand-in for future non-graph,
    non-repository subscription types (e.g. platform-level add-ons).
    """
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    subscription = self._build_active_subscription(
      resource_type="addon", resource_id="platform_pro"
    )
    mock_db.query.return_value.filter.return_value.first.return_value = subscription

    with pytest.raises(HTTPException) as exc:
      await cancel_subscription(
        org_id="org_123",
        subscription_id="sub_123",
        body=CancelSubscriptionRequest(immediate=True),
        current_user=mock_user,
        db=mock_db,
        _rate_limit=None,
      )

    assert exc.value.status_code == 400
    assert "confirm" in exc.value.detail.lower()
    subscription.cancel.assert_not_called()

  @pytest.mark.asyncio
  @patch("robosystems.routers.billing.subscriptions.BillingAuditLog")
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_immediate_wrong_confirm(
    self, mock_get_org_user, _mock_audit, mock_user, mock_db
  ):
    """Immediate cancel of a non-resource-scoped sub with mismatched confirm
    → 400."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    subscription = self._build_active_subscription(
      resource_type="addon", resource_id="platform_pro"
    )
    mock_db.query.return_value.filter.return_value.first.return_value = subscription

    with pytest.raises(HTTPException) as exc:
      await cancel_subscription(
        org_id="org_123",
        subscription_id="sub_123",
        body=CancelSubscriptionRequest(immediate=True, confirm="wrong"),
        current_user=mock_user,
        db=mock_db,
        _rate_limit=None,
      )

    assert exc.value.status_code == 400
    assert "confirm" in exc.value.detail.lower()
    subscription.cancel.assert_not_called()

  @pytest.mark.asyncio
  @patch("robosystems.routers.billing.subscriptions.get_payment_provider")
  @patch("robosystems.routers.billing.subscriptions.BillingAuditLog")
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_immediate_non_resource_scoped_success(
    self,
    mock_get_org_user,
    _mock_audit,
    mock_get_provider,
    mock_user,
    mock_db,
  ):
    """Immediate cancel of a hypothetical non-graph, non-repository sub still
    works (this is what the billing endpoint is reserved for going forward)."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    subscription = self._build_active_subscription(
      resource_type="addon", resource_id="platform_pro"
    )
    subscription.stripe_subscription_id = "sub_stripe_abc"
    mock_db.query.return_value.filter.return_value.first.return_value = subscription

    mock_provider = Mock()
    mock_get_provider.return_value = mock_provider

    await cancel_subscription(
      org_id="org_123",
      subscription_id="sub_123",
      body=CancelSubscriptionRequest(immediate=True, confirm="platform_pro"),
      current_user=mock_user,
      db=mock_db,
      _rate_limit=None,
    )

    mock_provider.cancel_subscription.assert_called_once_with("sub_stripe_abc")
    mock_provider.stripe.Subscription.modify.assert_not_called()
    subscription.cancel.assert_called_once_with(mock_db, immediate=True)

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_not_found(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Test canceling subscription that doesn't exist."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    mock_db.query.return_value.filter.return_value.first.return_value = None

    with pytest.raises(HTTPException) as exc:
      await cancel_subscription(
        org_id="org_123",
        subscription_id="sub_999",
        body=CancelSubscriptionRequest(),
        current_user=mock_user,
        db=mock_db,
        _rate_limit=None,
      )

    assert exc.value.status_code == 404
    assert "Subscription not found" in exc.value.detail

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_already_canceled(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Test canceling already canceled subscription."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    subscription = Mock(spec=BillingSubscription)
    subscription.status = "canceled"
    mock_db.query.return_value.filter.return_value.first.return_value = subscription

    with pytest.raises(HTTPException) as exc:
      await cancel_subscription(
        org_id="org_123",
        subscription_id="sub_123",
        body=CancelSubscriptionRequest(),
        current_user=mock_user,
        db=mock_db,
        _rate_limit=None,
      )

    assert exc.value.status_code == 400
    assert "already canceled" in exc.value.detail.lower()

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_canceling_status(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Test canceling subscription in canceling status."""
    from robosystems.models.core import OrgRole

    mock_org_user = Mock()
    mock_org_user.role = OrgRole.OWNER
    mock_get_org_user.return_value = mock_org_user

    subscription = Mock(spec=BillingSubscription)
    subscription.status = "canceling"
    mock_db.query.return_value.filter.return_value.first.return_value = subscription

    with pytest.raises(HTTPException) as exc:
      await cancel_subscription(
        org_id="org_123",
        subscription_id="sub_123",
        body=CancelSubscriptionRequest(),
        current_user=mock_user,
        db=mock_db,
        _rate_limit=None,
      )

    assert exc.value.status_code == 400
    assert "already canceled" in exc.value.detail.lower()

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_error_handling(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Test error handling in subscription cancellation."""
    mock_get_org_user.side_effect = Exception("Database error")

    with pytest.raises(HTTPException) as exc:
      await cancel_subscription(
        org_id="org_123",
        subscription_id="sub_123",
        body=CancelSubscriptionRequest(),
        current_user=mock_user,
        db=mock_db,
        _rate_limit=None,
      )

    assert exc.value.status_code == 500
    assert "Failed to cancel subscription" in exc.value.detail

  @pytest.mark.asyncio
  @patch("robosystems.models.core.OrgUser.get_by_org_and_user")
  async def test_cancel_subscription_requires_owner_role(
    self, mock_get_org_user, mock_user, mock_db
  ):
    """Admins should not be able to cancel subscriptions."""
    from robosystems.models.core import OrgRole

    mock_membership = Mock()
    mock_membership.role = OrgRole.ADMIN
    mock_get_org_user.return_value = mock_membership

    with pytest.raises(HTTPException) as exc:
      await cancel_subscription(
        org_id="org_123",
        subscription_id="sub_123",
        body=CancelSubscriptionRequest(),
        current_user=mock_user,
        db=mock_db,
        _rate_limit=None,
      )

    assert exc.value.status_code == 403
    assert exc.value.detail == "Only organization owners can cancel subscriptions"
