"""Unit tests for billing manager and CLI commands."""

import json
from unittest.mock import MagicMock, patch

import pytest
from botocore.exceptions import ClientError

from ai_org.cli import cli
from ai_org.core.billing_manager import BillingManager

# --- BillingManager unit tests ---


def _make_manager():
    """Create a BillingManager with mocked AWS clients."""
    mock_aws_client = MagicMock()
    mock_dynamodb = MagicMock()
    mock_secrets = MagicMock()
    mock_sts = MagicMock()

    def client_factory(service, **kwargs):
        if service == "dynamodb":
            return mock_dynamodb
        if service == "secretsmanager":
            return mock_secrets
        if service == "sts":
            return mock_sts
        return MagicMock()

    mock_aws_client.client = client_factory

    manager = BillingManager(mock_aws_client)
    return manager, mock_dynamodb, mock_secrets, mock_sts


@pytest.mark.unit
class TestGetStripeApiKey:
    def test_prefers_test_key_from_env(self):
        manager, _, _, _ = _make_manager()
        with patch.dict("os.environ", {"TEST_STRIPE_SECRET_KEY": "sk_test_abc123"}):
            assert manager._get_stripe_api_key() == "sk_test_abc123"

    def test_falls_back_to_secrets_manager(self):
        manager, _, mock_secrets, _ = _make_manager()
        mock_secrets.get_secret_value.return_value = {
            "SecretString": json.dumps({"api_key": "sk_live_from_sm"})
        }
        with (
            patch.dict(
                "os.environ",
                {"TEST_STRIPE_SECRET_KEY": "", "STRIPE_SECRET_KEY": ""},
                clear=False,
            ),
            patch("os.getenv", return_value=None),
        ):
            result = manager._get_stripe_api_key()
            assert result == "sk_live_from_sm"

    def test_falls_back_to_env_when_secret_not_found(self):
        manager, _, mock_secrets, _ = _make_manager()
        mock_secrets.get_secret_value.side_effect = ClientError(
            {"Error": {"Code": "ResourceNotFoundException", "Message": "not found"}},
            "GetSecretValue",
        )
        with (
            patch.dict("os.environ", {"STRIPE_SECRET_KEY": "sk_live_from_env"}, clear=False),
            patch("os.getenv") as mock_getenv,
        ):
            mock_getenv.side_effect = lambda _k, *a: {
                "TEST_STRIPE_SECRET_KEY": None,
                "STRIPE_SECRET_KEY": "sk_live_from_env",
            }.get(_k, a[0] if a else None)
            result = manager._get_stripe_api_key()
            assert result == "sk_live_from_env"

    def test_raises_when_no_key_available(self):
        manager, _, mock_secrets, _ = _make_manager()
        mock_secrets.get_secret_value.side_effect = ClientError(
            {"Error": {"Code": "ResourceNotFoundException", "Message": "not found"}},
            "GetSecretValue",
        )
        with (
            patch("os.getenv", return_value=None),
            pytest.raises(Exception, match="Stripe API key not found"),
        ):
            manager._get_stripe_api_key()


@pytest.mark.unit
class TestLinkAccount:
    def test_link_new_customer(self):
        manager, mock_ddb, _, _ = _make_manager()
        mock_ddb.get_item.return_value = {}  # No existing item

        manager.link_account("123456789012", "cus_abc123")

        mock_ddb.put_item.assert_called_once()
        call_args = mock_ddb.put_item.call_args
        item = call_args[1]["Item"]
        assert item["stripe_customer_id"]["S"] == "cus_abc123"
        assert item["aws_accounts"]["L"] == [{"S": "123456789012"}]

    def test_link_adds_to_existing_customer(self):
        manager, mock_ddb, _, _ = _make_manager()
        mock_ddb.get_item.return_value = {
            "Item": {
                "stripe_customer_id": {"S": "cus_abc123"},
                "aws_accounts": {"L": [{"S": "111111111111"}]},
            }
        }

        manager.link_account("222222222222", "cus_abc123")

        mock_ddb.update_item.assert_called_once()
        call_args = mock_ddb.update_item.call_args
        accounts = call_args[1]["ExpressionAttributeValues"][":accounts"]["L"]
        account_ids = [a["S"] for a in accounts]
        assert "111111111111" in account_ids
        assert "222222222222" in account_ids

    def test_link_skips_duplicate_account(self):
        manager, mock_ddb, _, _ = _make_manager()
        mock_ddb.get_item.return_value = {
            "Item": {
                "stripe_customer_id": {"S": "cus_abc123"},
                "aws_accounts": {"L": [{"S": "123456789012"}]},
            }
        }

        manager.link_account("123456789012", "cus_abc123")

        mock_ddb.update_item.assert_called_once()
        call_args = mock_ddb.update_item.call_args
        accounts = call_args[1]["ExpressionAttributeValues"][":accounts"]["L"]
        assert len(accounts) == 1

    def test_link_raises_on_missing_table(self):
        manager, mock_ddb, _, _ = _make_manager()
        mock_ddb.get_item.side_effect = ClientError(
            {"Error": {"Code": "ResourceNotFoundException", "Message": "table not found"}},
            "GetItem",
        )
        with pytest.raises(Exception, match="DynamoDB table.*not found"):
            manager.link_account("123456789012", "cus_abc123")


@pytest.mark.unit
class TestUnlinkAccount:
    def test_unlink_removes_from_multi_account(self):
        manager, mock_ddb, _, _ = _make_manager()
        mock_ddb.scan.return_value = {
            "Items": [
                {
                    "stripe_customer_id": {"S": "cus_abc123"},
                    "aws_accounts": {"L": [{"S": "111111111111"}, {"S": "222222222222"}]},
                }
            ]
        }

        manager.unlink_account("111111111111")

        mock_ddb.update_item.assert_called_once()
        call_args = mock_ddb.update_item.call_args
        accounts = call_args[1]["ExpressionAttributeValues"][":accounts"]["L"]
        assert accounts == [{"S": "222222222222"}]

    def test_unlink_deletes_mapping_when_last_account(self):
        manager, mock_ddb, _, _ = _make_manager()
        mock_ddb.scan.return_value = {
            "Items": [
                {
                    "stripe_customer_id": {"S": "cus_abc123"},
                    "aws_accounts": {"L": [{"S": "123456789012"}]},
                }
            ]
        }

        manager.unlink_account("123456789012")

        mock_ddb.delete_item.assert_called_once()
        mock_ddb.update_item.assert_not_called()

    def test_unlink_raises_when_account_not_found(self):
        manager, mock_ddb, _, _ = _make_manager()
        mock_ddb.scan.return_value = {"Items": []}

        with pytest.raises(Exception, match="not found in any billing mappings"):
            manager.unlink_account("999999999999")


@pytest.mark.unit
class TestListMappings:
    def test_list_returns_sorted_mappings(self):
        manager, mock_ddb, _, _ = _make_manager()
        mock_ddb.scan.return_value = {
            "Items": [
                {
                    "stripe_customer_id": {"S": "cus_bbb"},
                    "aws_accounts": {"L": [{"S": "222222222222"}]},
                    "created_at": {"S": "2026-02-01T00:00:00"},
                },
                {
                    "stripe_customer_id": {"S": "cus_aaa"},
                    "aws_accounts": {"L": [{"S": "111111111111"}]},
                    "created_at": {"S": "2026-01-01T00:00:00"},
                },
            ]
        }

        result = manager.list_mappings()

        assert len(result) == 2
        assert result[0]["stripe_customer_id"] == "cus_aaa"
        assert result[1]["stripe_customer_id"] == "cus_bbb"

    def test_list_returns_empty_when_no_mappings(self):
        manager, mock_ddb, _, _ = _make_manager()
        mock_ddb.scan.return_value = {"Items": []}

        result = manager.list_mappings()
        assert result == []

    def test_list_includes_timestamps(self):
        manager, mock_ddb, _, _ = _make_manager()
        mock_ddb.scan.return_value = {
            "Items": [
                {
                    "stripe_customer_id": {"S": "cus_aaa"},
                    "aws_accounts": {"L": [{"S": "111111111111"}]},
                    "created_at": {"S": "2026-01-01T00:00:00"},
                    "updated_at": {"S": "2026-02-01T00:00:00"},
                }
            ]
        }

        result = manager.list_mappings()
        assert result[0]["created_at"] == "2026-01-01T00:00:00"
        assert result[0]["updated_at"] == "2026-02-01T00:00:00"

    def test_list_raises_on_missing_table(self):
        manager, mock_ddb, _, _ = _make_manager()
        mock_ddb.scan.side_effect = ClientError(
            {"Error": {"Code": "ResourceNotFoundException", "Message": "table not found"}},
            "Scan",
        )
        with pytest.raises(Exception, match="DynamoDB table.*not found"):
            manager.list_mappings()


# --- CLI command tests ---


@pytest.mark.unit
class TestBillingLinkCommand:
    def test_link_validates_account_id_format(self, runner):
        with patch("ai_org.core.aws_client.boto3.Session"):
            result = runner.invoke(cli, ["billing", "link", "bad-id", "cus_abc123"])
            assert result.exit_code != 0

    def test_link_validates_stripe_customer_id_format(self, runner):
        with patch("ai_org.core.aws_client.boto3.Session"):
            result = runner.invoke(cli, ["billing", "link", "123456789012", "bad_id"])
            assert result.exit_code != 0

    def test_link_success(self, runner):
        with (
            patch("ai_org.core.aws_client.boto3.Session"),
            patch.object(BillingManager, "link_account") as mock_link,
        ):
            result = runner.invoke(cli, ["billing", "link", "123456789012", "cus_abc123"])
            assert result.exit_code == 0
            mock_link.assert_called_once_with("123456789012", "cus_abc123")


@pytest.mark.unit
class TestBillingUnlinkCommand:
    def test_unlink_validates_account_id(self, runner):
        with patch("ai_org.core.aws_client.boto3.Session"):
            result = runner.invoke(cli, ["billing", "unlink", "bad"])
            assert result.exit_code != 0

    def test_unlink_success(self, runner):
        with (
            patch("ai_org.core.aws_client.boto3.Session"),
            patch.object(BillingManager, "unlink_account") as mock_unlink,
        ):
            result = runner.invoke(cli, ["billing", "unlink", "123456789012"])
            assert result.exit_code == 0
            mock_unlink.assert_called_once_with("123456789012")


@pytest.mark.unit
class TestBillingListCommand:
    def test_list_empty(self, runner):
        with (
            patch("ai_org.core.aws_client.boto3.Session"),
            patch.object(BillingManager, "list_mappings", return_value=[]),
        ):
            result = runner.invoke(cli, ["billing", "list"])
            assert result.exit_code == 0
            assert "No billing mappings" in result.output

    def test_list_with_mappings(self, runner):
        mappings = [
            {
                "stripe_customer_id": "cus_abc",
                "aws_accounts": ["123456789012"],
                "created_at": "2026-01-01T00:00:00",
            }
        ]
        with (
            patch("ai_org.core.aws_client.boto3.Session"),
            patch.object(BillingManager, "list_mappings", return_value=mappings),
        ):
            result = runner.invoke(cli, ["billing", "list"])
            assert result.exit_code == 0
            assert "1 billing mapping" in result.output
