"""Tests for the Pass payment SDK."""

import os
from unittest.mock import patch

import httpx
import pytest

from mongolian_payment_pass import (
    AsyncPassClient,
    OrderInquiryStatusCancelled,
    OrderInquiryStatusPaid,
    OrderInquiryStatusPending,
    OrderInquiryStatusVoided,
    PassClient,
    PassConfig,
    PassError,
    WebhookOperationPayment,
    WebhookOperationVoid,
    load_config_from_env,
)

# -- Fixtures --


@pytest.fixture
def config() -> PassConfig:
    return PassConfig(
        endpoint="https://api.pass.test/",
        ecommerce_token="test-token-123",
        callback="https://example.com/callback",
    )


# ============================================================================
# Constants Tests
# ============================================================================


class TestConstants:
    """Tests for status and operation constants."""

    def test_order_inquiry_status_values(self) -> None:
        assert OrderInquiryStatusPaid == "paid"
        assert OrderInquiryStatusPending == "pending"
        assert OrderInquiryStatusCancelled == "cancelled"
        assert OrderInquiryStatusVoided == "voided"

    def test_webhook_operation_values(self) -> None:
        assert WebhookOperationPayment == "payment"
        assert WebhookOperationVoid == "void"


# ============================================================================
# Sync Client Tests
# ============================================================================


class TestPassClient:
    """Tests for the synchronous PassClient."""

    def test_constructor_strips_trailing_slash(self, config: PassConfig) -> None:
        client = PassClient(config)
        assert client._endpoint == "https://api.pass.test"

    def test_constructor_strips_multiple_trailing_slashes(self) -> None:
        config = PassConfig(
            endpoint="https://api.pass.test///",
            ecommerce_token="tok",
            callback="https://example.com/cb",
        )
        client = PassClient(config)
        assert client._endpoint == "https://api.pass.test"

    def test_constructor_requires_endpoint(self) -> None:
        with pytest.raises(ValueError, match="endpoint is required"):
            PassClient(
                PassConfig(endpoint="", ecommerce_token="tok", callback="cb")
            )

    def test_constructor_requires_ecommerce_token(self) -> None:
        with pytest.raises(ValueError, match="ecommerce_token is required"):
            PassClient(
                PassConfig(
                    endpoint="https://api.pass.test",
                    ecommerce_token="",
                    callback="cb",
                )
            )

    def test_constructor_requires_callback(self) -> None:
        with pytest.raises(ValueError, match="callback is required"):
            PassClient(
                PassConfig(
                    endpoint="https://api.pass.test",
                    ecommerce_token="tok",
                    callback="",
                )
            )

    def test_create_order(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status_code": "200",
                "ret": {
                    "shop": "Test Shop",
                    "amount": "150000",
                    "order_id": "order-abc",
                    "order_ttl": 3600,
                    "db_ref_no": "ref-123",
                },
            },
            request=httpx.Request("POST", "https://api.pass.test/create_order"),
        )

        client = PassClient(config)

        with patch.object(client._client, "post", return_value=mock_response) as mock:
            result = client.create_order(1500)

            mock.assert_called_once()
            call_kwargs = mock.call_args
            body = call_kwargs.kwargs["json"]

            assert body["ecommerce_token"] == "test-token-123"
            assert body["amount"] == 150000
            assert body["callback_url"] == "https://example.com/callback"

        assert result.shop == "Test Shop"
        assert result.amount == "150000"
        assert result.order_id == "order-abc"
        assert result.order_ttl == 3600
        assert result.db_ref_no == "ref-123"

    def test_create_order_multiplies_amount_by_100(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={"status_code": "200", "ret": {
                "shop": "", "amount": "", "order_id": "", "order_ttl": 0, "db_ref_no": "",
            }},
            request=httpx.Request("POST", "https://api.pass.test/create_order"),
        )

        client = PassClient(config)

        with patch.object(client._client, "post", return_value=mock_response) as mock:
            client.create_order(25.5)
            body = mock.call_args.kwargs["json"]
            assert body["amount"] == 2550.0

    def test_create_order_with_callback_params(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={"status_code": "200", "ret": {
                "shop": "", "amount": "", "order_id": "", "order_ttl": 0, "db_ref_no": "",
            }},
            request=httpx.Request("POST", "https://api.pass.test/create_order"),
        )

        client = PassClient(config)

        with patch.object(client._client, "post", return_value=mock_response) as mock:
            client.create_order(100, callback_params={"ref": "abc", "user": "123"})
            body = mock.call_args.kwargs["json"]
            callback_url = body["callback_url"]
            assert callback_url.startswith("https://example.com/callback?")
            assert "ref=abc" in callback_url
            assert "user=123" in callback_url

    def test_create_order_with_callback_params_existing_query(self) -> None:
        config = PassConfig(
            endpoint="https://api.pass.test",
            ecommerce_token="tok",
            callback="https://example.com/callback?existing=1",
        )
        mock_response = httpx.Response(
            status_code=200,
            json={"status_code": "200", "ret": {
                "shop": "", "amount": "", "order_id": "", "order_ttl": 0, "db_ref_no": "",
            }},
            request=httpx.Request("POST", "https://api.pass.test/create_order"),
        )

        client = PassClient(config)

        with patch.object(client._client, "post", return_value=mock_response) as mock:
            client.create_order(100, callback_params={"ref": "abc"})
            body = mock.call_args.kwargs["json"]
            callback_url = body["callback_url"]
            assert "existing=1" in callback_url
            assert "&ref=abc" in callback_url

    def test_inquiry_order(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status_code": "200",
                "ret": {
                    "resp_code": "00",
                    "resp_msg": "Success",
                    "status": "paid",
                    "amount": "150000",
                    "db_ref_no": "ref-123",
                    "status_text": "Paid",
                },
            },
            request=httpx.Request("POST", "https://api.pass.test/order_inquiry"),
        )

        client = PassClient(config)

        with patch.object(client._client, "post", return_value=mock_response) as mock:
            result = client.inquiry_order("order-abc")

            body = mock.call_args.kwargs["json"]
            assert body["ecommerce_token"] == "test-token-123"
            assert body["order_id"] == "order-abc"

        assert result.resp_code == "00"
        assert result.resp_msg == "Success"
        assert result.status == "paid"
        assert result.amount == "150000"
        assert result.db_ref_no == "ref-123"
        assert result.status_text == "Paid"

    def test_notify_order(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status_code": "200",
                "ret": {
                    "resp_code": "00",
                    "resp_msg": "Success",
                    "success": 1,
                    "data": [
                        {"success": True, "message_id": "msg-001"},
                    ],
                },
            },
            request=httpx.Request("POST", "https://api.pass.test/order_notify"),
        )

        client = PassClient(config)

        with patch.object(client._client, "post", return_value=mock_response) as mock:
            result = client.notify_order("order-abc", "99112233")

            body = mock.call_args.kwargs["json"]
            assert body["ecommerce_token"] == "test-token-123"
            assert body["order_id"] == "order-abc"
            assert body["phone"] == "99112233"

        assert result.resp_code == "00"
        assert result.success == 1
        assert len(result.data) == 1
        assert result.data[0].success is True
        assert result.data[0].message_id == "msg-001"

    def test_cancel_order(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status_code": "200",
                "ret": {
                    "resp_code": "00",
                    "resp_msg": "Cancelled",
                    "status": "cancelled",
                    "amount": "150000",
                    "loyalty_data": None,
                    "db_ref_no": "ref-123",
                },
            },
            request=httpx.Request("POST", "https://api.pass.test/cancel_order"),
        )

        client = PassClient(config)

        with patch.object(client._client, "post", return_value=mock_response) as mock:
            result = client.cancel_order("order-abc")

            body = mock.call_args.kwargs["json"]
            assert body["order_id"] == "order-abc"

        assert result.resp_code == "00"
        assert result.status == "cancelled"
        assert result.db_ref_no == "ref-123"

    def test_void_order(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status_code": "200",
                "ret": {
                    "resp_code": "00",
                    "resp_msg": "Voided",
                    "status": "voided",
                    "amount": "150000",
                    "loyalty_data": None,
                    "db_ref_no": "ref-123",
                    "service_name": "pass",
                    "date_time": "2026-01-01 12:00:00",
                    "trace_no": "trace-001",
                    "rrn": "rrn-001",
                    "terminal_id": "term-001",
                    "merchant_id": "merch-001",
                },
            },
            request=httpx.Request("POST", "https://api.pass.test/void"),
        )

        client = PassClient(config)

        with patch.object(client._client, "post", return_value=mock_response) as mock:
            result = client.void_order("order-abc")

            body = mock.call_args.kwargs["json"]
            assert body["order_id"] == "order-abc"

        assert result.resp_code == "00"
        assert result.status == "voided"
        assert result.service_name == "pass"
        assert result.terminal_id == "term-001"
        assert result.merchant_id == "merch-001"

    def test_api_error_raises_pass_error(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status_code": "400",
                "msg": {
                    "code": "INVALID_TOKEN",
                    "level": "error",
                    "body": "Token is invalid",
                },
            },
            request=httpx.Request("POST", "https://api.pass.test/create_order"),
        )

        client = PassClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            with pytest.raises(PassError, match="INVALID_TOKEN") as exc_info:
                client.create_order(100)

            assert exc_info.value.code == "INVALID_TOKEN"
            assert exc_info.value.level == "error"
            assert exc_info.value.body == "Token is invalid"

    def test_http_error_raises_pass_error(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=500,
            json={"error": "Internal Server Error"},
            request=httpx.Request("POST", "https://api.pass.test/create_order"),
        )

        client = PassClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            with pytest.raises(PassError, match="HTTP 500") as exc_info:
                client.create_order(100)

            assert exc_info.value.status_code == 500
            assert exc_info.value.response == {"error": "Internal Server Error"}

    def test_network_error_raises_pass_error(self, config: PassConfig) -> None:
        client = PassClient(config)

        with patch.object(
            client._client,
            "post",
            side_effect=httpx.ConnectError("Connection refused"),
        ):
            with pytest.raises(PassError, match="Network error"):
                client.create_order(100)

    def test_invalid_json_raises_pass_error(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            content=b"not json",
            request=httpx.Request("POST", "https://api.pass.test/create_order"),
        )

        client = PassClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            with pytest.raises(PassError, match="Invalid JSON"):
                client.create_order(100)

    def test_context_manager(self, config: PassConfig) -> None:
        with PassClient(config) as client:
            assert client._endpoint == "https://api.pass.test"

    def test_parse_webhook_valid(self) -> None:
        payload = {
            "order_id": "order-abc",
            "payment_request_id": "pr-001",
            "pos_id": "pos-001",
            "operation": "payment",
            "is_success": True,
            "amount": "150000",
            "created_time": "2026-01-01T12:00:00Z",
            "db_ref_no": "ref-123",
        }

        result = PassClient.parse_webhook(payload)

        assert result.order_id == "order-abc"
        assert result.payment_request_id == "pr-001"
        assert result.pos_id == "pos-001"
        assert result.operation == "payment"
        assert result.is_success is True
        assert result.amount == "150000"
        assert result.created_time == "2026-01-01T12:00:00Z"
        assert result.db_ref_no == "ref-123"

    def test_parse_webhook_with_customer_data(self) -> None:
        payload = {
            "order_id": "order-abc",
            "payment_request_id": "pr-001",
            "pos_id": "pos-001",
            "operation": "payment",
            "is_success": True,
            "amount": "150000",
            "created_time": "2026-01-01T12:00:00Z",
            "db_ref_no": "ref-123",
            "customer_data": {
                "user_id": "user-001",
                "unique_id": "uid-001",
            },
        }

        result = PassClient.parse_webhook(payload)
        assert result.customer_data is not None
        assert result.customer_data.user_id == "user-001"
        assert result.customer_data.unique_id == "uid-001"

    def test_parse_webhook_invalid_none(self) -> None:
        with pytest.raises(PassError, match="expected an object"):
            PassClient.parse_webhook(None)

    def test_parse_webhook_invalid_not_dict(self) -> None:
        with pytest.raises(PassError, match="expected an object"):
            PassClient.parse_webhook("not a dict")

    def test_parse_webhook_missing_order_id(self) -> None:
        with pytest.raises(PassError, match="missing or invalid order_id"):
            PassClient.parse_webhook({"payment_request_id": "pr-001"})

    def test_parse_webhook_empty_order_id(self) -> None:
        with pytest.raises(PassError, match="missing or invalid order_id"):
            PassClient.parse_webhook({"order_id": ""})

    def test_parse_webhook_non_string_order_id(self) -> None:
        with pytest.raises(PassError, match="missing or invalid order_id"):
            PassClient.parse_webhook({"order_id": 12345})


# ============================================================================
# Async Client Tests
# ============================================================================


class TestAsyncPassClient:
    """Tests for the asynchronous AsyncPassClient."""

    @pytest.mark.asyncio
    async def test_create_order(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status_code": "200",
                "ret": {
                    "shop": "Async Shop",
                    "amount": "200000",
                    "order_id": "async-order-123",
                    "order_ttl": 7200,
                    "db_ref_no": "async-ref-001",
                },
            },
            request=httpx.Request("POST", "https://api.pass.test/create_order"),
        )

        client = AsyncPassClient(config)

        with patch.object(client._client, "post", return_value=mock_response) as mock:
            result = await client.create_order(2000)

            body = mock.call_args.kwargs["json"]
            assert body["amount"] == 200000

        assert result.order_id == "async-order-123"
        assert result.shop == "Async Shop"

        await client.close()

    @pytest.mark.asyncio
    async def test_inquiry_order(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status_code": "200",
                "ret": {
                    "resp_code": "00",
                    "resp_msg": "OK",
                    "status": "paid",
                    "amount": "200000",
                    "db_ref_no": "ref-async",
                    "status_text": "Paid",
                },
            },
            request=httpx.Request("POST", "https://api.pass.test/order_inquiry"),
        )

        client = AsyncPassClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            result = await client.inquiry_order("async-order-123")

        assert result.status == "paid"
        assert result.resp_code == "00"

        await client.close()

    @pytest.mark.asyncio
    async def test_notify_order(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status_code": "200",
                "ret": {
                    "resp_code": "00",
                    "resp_msg": "OK",
                    "success": 1,
                    "data": [{"success": True, "message_id": "msg-async"}],
                },
            },
            request=httpx.Request("POST", "https://api.pass.test/order_notify"),
        )

        client = AsyncPassClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            result = await client.notify_order("async-order-123", "99001122")

        assert result.success == 1
        assert result.data[0].message_id == "msg-async"

        await client.close()

    @pytest.mark.asyncio
    async def test_cancel_order(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status_code": "200",
                "ret": {
                    "resp_code": "00",
                    "resp_msg": "Cancelled",
                    "status": "cancelled",
                    "amount": "200000",
                    "loyalty_data": None,
                    "db_ref_no": "ref-async",
                },
            },
            request=httpx.Request("POST", "https://api.pass.test/cancel_order"),
        )

        client = AsyncPassClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            result = await client.cancel_order("async-order-123")

        assert result.status == "cancelled"

        await client.close()

    @pytest.mark.asyncio
    async def test_void_order(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status_code": "200",
                "ret": {
                    "resp_code": "00",
                    "resp_msg": "Voided",
                    "status": "voided",
                    "amount": "200000",
                    "loyalty_data": None,
                    "db_ref_no": "ref-async",
                    "service_name": "pass",
                    "date_time": "2026-01-01",
                    "trace_no": "t-001",
                    "rrn": "r-001",
                    "terminal_id": "term-001",
                    "merchant_id": "merch-001",
                },
            },
            request=httpx.Request("POST", "https://api.pass.test/void"),
        )

        client = AsyncPassClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            result = await client.void_order("async-order-123")

        assert result.status == "voided"
        assert result.merchant_id == "merch-001"

        await client.close()

    @pytest.mark.asyncio
    async def test_http_error_raises_pass_error(self, config: PassConfig) -> None:
        mock_response = httpx.Response(
            status_code=403,
            json={"error": "Forbidden"},
            request=httpx.Request("POST", "https://api.pass.test/create_order"),
        )

        client = AsyncPassClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            with pytest.raises(PassError, match="HTTP 403"):
                await client.create_order(100)

        await client.close()

    @pytest.mark.asyncio
    async def test_network_error_raises_pass_error(self, config: PassConfig) -> None:
        client = AsyncPassClient(config)

        with patch.object(
            client._client,
            "post",
            side_effect=httpx.ConnectError("Connection refused"),
        ):
            with pytest.raises(PassError, match="Network error"):
                await client.create_order(100)

        await client.close()

    @pytest.mark.asyncio
    async def test_async_context_manager(self, config: PassConfig) -> None:
        async with AsyncPassClient(config) as client:
            assert client._endpoint == "https://api.pass.test"

    @pytest.mark.asyncio
    async def test_parse_webhook(self) -> None:
        payload = {
            "order_id": "order-abc",
            "payment_request_id": "pr-001",
            "pos_id": "pos-001",
            "operation": "payment",
            "is_success": True,
            "amount": "150000",
            "created_time": "2026-01-01T12:00:00Z",
            "db_ref_no": "ref-123",
        }

        result = AsyncPassClient.parse_webhook(payload)
        assert result.order_id == "order-abc"


# ============================================================================
# Config Tests
# ============================================================================


class TestLoadConfigFromEnv:
    """Tests for load_config_from_env."""

    def test_loads_all_env_vars(self) -> None:
        env = {
            "PASS_ENDPOINT": "https://api.pass.test",
            "PASS_ECOMMERCE_TOKEN": "env-token",
            "PASS_CALLBACK": "https://example.com/cb",
        }
        with patch.dict(os.environ, env, clear=False):
            cfg = load_config_from_env()

        assert cfg.endpoint == "https://api.pass.test"
        assert cfg.ecommerce_token == "env-token"
        assert cfg.callback == "https://example.com/cb"

    def test_missing_endpoint_raises(self) -> None:
        env = {
            "PASS_ECOMMERCE_TOKEN": "tok",
            "PASS_CALLBACK": "cb",
        }
        with patch.dict(os.environ, env, clear=False):
            os.environ.pop("PASS_ENDPOINT", None)
            with pytest.raises(ValueError, match="PASS_ENDPOINT"):
                load_config_from_env()

    def test_missing_ecommerce_token_raises(self) -> None:
        env = {
            "PASS_ENDPOINT": "https://api.pass.test",
            "PASS_CALLBACK": "cb",
        }
        with patch.dict(os.environ, env, clear=False):
            os.environ.pop("PASS_ECOMMERCE_TOKEN", None)
            with pytest.raises(ValueError, match="PASS_ECOMMERCE_TOKEN"):
                load_config_from_env()

    def test_missing_callback_raises(self) -> None:
        env = {
            "PASS_ENDPOINT": "https://api.pass.test",
            "PASS_ECOMMERCE_TOKEN": "tok",
        }
        with patch.dict(os.environ, env, clear=False):
            os.environ.pop("PASS_CALLBACK", None)
            with pytest.raises(ValueError, match="PASS_CALLBACK"):
                load_config_from_env()


# ============================================================================
# Error Tests
# ============================================================================


class TestPassError:
    """Tests for PassError."""

    def test_basic_error(self) -> None:
        err = PassError("something went wrong")
        assert str(err) == "something went wrong"
        assert err.code is None
        assert err.level is None
        assert err.body is None
        assert err.status_code is None
        assert err.response is None

    def test_error_with_api_fields(self) -> None:
        err = PassError(
            "api error",
            code="ERR_001",
            level="error",
            body="Bad request",
        )
        assert err.code == "ERR_001"
        assert err.level == "error"
        assert err.body == "Bad request"

    def test_error_with_status_code(self) -> None:
        err = PassError("bad request", status_code=400)
        assert err.status_code == 400

    def test_error_with_response(self) -> None:
        body = {"error": "details"}
        err = PassError("api error", status_code=500, response=body)
        assert err.status_code == 500
        assert err.response == body

    def test_is_exception(self) -> None:
        err = PassError("test")
        assert isinstance(err, Exception)
