"""Tests for the MongolChat payment SDK."""

import os
from unittest.mock import patch

import httpx
import pytest

from mongolian_payment_mongolchat import (
    AsyncMongolChatClient,
    MchatGenerateQrInput,
    MchatProduct,
    MongolChatClient,
    MongolChatConfig,
    MongolChatError,
    load_config_from_env,
)

# ── Fixtures ──


@pytest.fixture
def config() -> MongolChatConfig:
    return MongolChatConfig(
        endpoint="https://api.mongolchat.test/",
        api_key="test-api-key",
        worker_key="test-worker-key",
        app_secret="test-app-secret",
        branch_no="001",
    )


@pytest.fixture
def generate_qr_input() -> MchatGenerateQrInput:
    return MchatGenerateQrInput(
        amount=5000,
        products=[
            MchatProduct(
                product_name="Coffee",
                quantity="1",
                price=5000,
                tag="food",
            )
        ],
        title="Coffee Shop",
        sub_title="Order #123",
        noat="123456",
        nhat="654321",
        ttd="789",
        reference_number="REF-001",
        expire_time="2026-12-31T23:59:59",
    )


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


class TestMongolChatClient:
    """Tests for the synchronous MongolChatClient."""

    def test_constructor_strips_trailing_slash(self, config: MongolChatConfig) -> None:
        client = MongolChatClient(config)
        assert client._endpoint == "https://api.mongolchat.test"

    def test_constructor_strips_multiple_trailing_slashes(self) -> None:
        cfg = MongolChatConfig(
            endpoint="https://api.mongolchat.test///",
            api_key="key",
            worker_key="wk",
            app_secret="secret",
            branch_no="001",
        )
        client = MongolChatClient(cfg)
        assert client._endpoint == "https://api.mongolchat.test"

    def test_constructor_requires_endpoint(self) -> None:
        with pytest.raises(ValueError, match="endpoint is required"):
            MongolChatClient(
                MongolChatConfig(
                    endpoint="",
                    api_key="key",
                    worker_key="wk",
                    app_secret="secret",
                    branch_no="001",
                )
            )

    def test_constructor_requires_api_key(self) -> None:
        with pytest.raises(ValueError, match="api_key is required"):
            MongolChatClient(
                MongolChatConfig(
                    endpoint="https://api.mongolchat.test",
                    api_key="",
                    worker_key="wk",
                    app_secret="secret",
                    branch_no="001",
                )
            )

    def test_constructor_requires_worker_key(self) -> None:
        with pytest.raises(ValueError, match="worker_key is required"):
            MongolChatClient(
                MongolChatConfig(
                    endpoint="https://api.mongolchat.test",
                    api_key="key",
                    worker_key="",
                    app_secret="secret",
                    branch_no="001",
                )
            )

    def test_constructor_requires_app_secret(self) -> None:
        with pytest.raises(ValueError, match="app_secret is required"):
            MongolChatClient(
                MongolChatConfig(
                    endpoint="https://api.mongolchat.test",
                    api_key="key",
                    worker_key="wk",
                    app_secret="",
                    branch_no="001",
                )
            )

    def test_constructor_requires_branch_no(self) -> None:
        with pytest.raises(ValueError, match="branch_no is required"):
            MongolChatClient(
                MongolChatConfig(
                    endpoint="https://api.mongolchat.test",
                    api_key="key",
                    worker_key="wk",
                    app_secret="secret",
                    branch_no="",
                )
            )

    def test_headers(self, config: MongolChatConfig) -> None:
        client = MongolChatClient(config)
        headers = client._headers()
        assert headers["api-key"] == "test-api-key"
        assert headers["Authorization"] == "WorkerKey test-worker-key"
        assert headers["Content-Type"] == "application/json"

    def test_generate_qr(
        self,
        config: MongolChatConfig,
        generate_qr_input: MchatGenerateQrInput,
    ) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "qr": "qr-code-string-123",
                "code": 1000,
                "message": "success",
            },
            request=httpx.Request(
                "POST",
                "https://api.mongolchat.test/worker/onlineqr/generate",
            ),
        )

        client = MongolChatClient(config)

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

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

            assert body["amount"] == 5000
            assert body["title"] == "Coffee Shop"
            assert body["sub_title"] == "Order #123"
            assert body["noat"] == "123456"
            assert body["nhat"] == "654321"
            assert body["ttd"] == "789"
            assert body["reference_number"] == "REF-001"
            assert body["expire_time"] == "2026-12-31T23:59:59"
            assert len(body["products"]) == 1
            assert body["products"][0]["product_name"] == "Coffee"
            assert body["products"][0]["quantity"] == "1"
            assert body["products"][0]["price"] == 5000
            assert body["products"][0]["tag"] == "food"
            assert "branch_id" not in body

        assert result.qr == "qr-code-string-123"
        assert result.code == 1000
        assert result.message == "success"

    def test_generate_qr_with_branch_id(
        self,
        config: MongolChatConfig,
    ) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={"qr": "qr-123", "code": 1000, "message": "success"},
            request=httpx.Request(
                "POST",
                "https://api.mongolchat.test/worker/onlineqr/generate",
            ),
        )

        client = MongolChatClient(config)

        input_with_branch = MchatGenerateQrInput(
            amount=1000,
            products=[],
            title="Test",
            sub_title="Sub",
            noat="1",
            nhat="2",
            ttd="3",
            reference_number="REF-002",
            expire_time="2026-12-31T23:59:59",
            branch_id="branch-42",
        )

        with patch.object(client._client, "post", return_value=mock_response) as mock:
            client.generate_qr(input_with_branch)

            body = mock.call_args.kwargs["json"]
            assert body["branch_id"] == "branch-42"

    def test_generate_qr_api_error(
        self,
        config: MongolChatConfig,
        generate_qr_input: MchatGenerateQrInput,
    ) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "code": 2001,
                "message": "Invalid amount",
            },
            request=httpx.Request(
                "POST",
                "https://api.mongolchat.test/worker/onlineqr/generate",
            ),
        )

        client = MongolChatClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            with pytest.raises(MongolChatError, match="MongolChat error") as exc_info:
                client.generate_qr(generate_qr_input)

            assert exc_info.value.code == 2001
            assert exc_info.value.response["message"] == "Invalid amount"

    def test_check_qr(self, config: MongolChatConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status": "paid",
                "code": 1000,
                "message": "success",
                "id": "txn-abc",
                "who_paid": "user-123",
                "user_ref_id": "ref-456",
            },
            request=httpx.Request(
                "POST",
                "https://api.mongolchat.test/worker/onlineqr/status",
            ),
        )

        client = MongolChatClient(config)

        with patch.object(client._client, "post", return_value=mock_response) as mock:
            result = client.check_qr("qr-code-string-123")

            mock.assert_called_once()
            body = mock.call_args.kwargs["json"]
            assert body == {"qr": "qr-code-string-123"}

        assert result.status == "paid"
        assert result.code == 1000
        assert result.message == "success"
        assert result.id == "txn-abc"
        assert result.who_paid == "user-123"
        assert result.user_ref_id == "ref-456"

    def test_check_qr_api_error(self, config: MongolChatConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "code": 3001,
                "message": "QR not found",
            },
            request=httpx.Request(
                "POST",
                "https://api.mongolchat.test/worker/onlineqr/status",
            ),
        )

        client = MongolChatClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            with pytest.raises(MongolChatError, match="MongolChat error") as exc_info:
                client.check_qr("invalid-qr")

            assert exc_info.value.code == 3001

    def test_network_error_raises_mongolchat_error(
        self, config: MongolChatConfig
    ) -> None:
        client = MongolChatClient(config)

        with patch.object(
            client._client,
            "post",
            side_effect=httpx.ConnectError("Connection refused"),
        ):
            with pytest.raises(MongolChatError, match="Network error"):
                client.check_qr("some-qr")

    def test_invalid_json_raises_mongolchat_error(
        self, config: MongolChatConfig
    ) -> None:
        mock_response = httpx.Response(
            status_code=200,
            content=b"not json",
            request=httpx.Request(
                "POST",
                "https://api.mongolchat.test/worker/onlineqr/status",
            ),
        )

        client = MongolChatClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            with pytest.raises(MongolChatError, match="Invalid JSON"):
                client.check_qr("some-qr")

    def test_context_manager(self, config: MongolChatConfig) -> None:
        with MongolChatClient(config) as client:
            assert client._endpoint == "https://api.mongolchat.test"

    def test_missing_fields_default_to_empty(self, config: MongolChatConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={"code": 1000, "message": "success"},
            request=httpx.Request(
                "POST",
                "https://api.mongolchat.test/worker/onlineqr/status",
            ),
        )

        client = MongolChatClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            result = client.check_qr("some-qr")

        assert result.status == ""
        assert result.id == ""
        assert result.who_paid == ""
        assert result.user_ref_id == ""


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


class TestAsyncMongolChatClient:
    """Tests for the asynchronous AsyncMongolChatClient."""

    @pytest.mark.asyncio
    async def test_generate_qr(
        self,
        config: MongolChatConfig,
        generate_qr_input: MchatGenerateQrInput,
    ) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "qr": "async-qr-123",
                "code": 1000,
                "message": "success",
            },
            request=httpx.Request(
                "POST",
                "https://api.mongolchat.test/worker/onlineqr/generate",
            ),
        )

        client = AsyncMongolChatClient(config)

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

            mock.assert_called_once()
            body = mock.call_args.kwargs["json"]
            assert body["amount"] == 5000

        assert result.qr == "async-qr-123"
        assert result.code == 1000

        await client.close()

    @pytest.mark.asyncio
    async def test_check_qr_success(self, config: MongolChatConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "status": "paid",
                "code": 1000,
                "message": "success",
                "id": "async-txn",
                "who_paid": "async-user",
                "user_ref_id": "async-ref",
            },
            request=httpx.Request(
                "POST",
                "https://api.mongolchat.test/worker/onlineqr/status",
            ),
        )

        client = AsyncMongolChatClient(config)

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

        assert result.status == "paid"
        assert result.id == "async-txn"
        assert result.who_paid == "async-user"

        await client.close()

    @pytest.mark.asyncio
    async def test_check_qr_api_error(self, config: MongolChatConfig) -> None:
        mock_response = httpx.Response(
            status_code=200,
            json={
                "code": 4001,
                "message": "Expired",
            },
            request=httpx.Request(
                "POST",
                "https://api.mongolchat.test/worker/onlineqr/status",
            ),
        )

        client = AsyncMongolChatClient(config)

        with patch.object(client._client, "post", return_value=mock_response):
            with pytest.raises(MongolChatError, match="MongolChat error"):
                await client.check_qr("expired-qr")

        await client.close()

    @pytest.mark.asyncio
    async def test_network_error_raises_mongolchat_error(
        self, config: MongolChatConfig
    ) -> None:
        client = AsyncMongolChatClient(config)

        with patch.object(
            client._client,
            "post",
            side_effect=httpx.ConnectError("Connection refused"),
        ):
            with pytest.raises(MongolChatError, match="Network error"):
                await client.check_qr("unreachable-qr")

        await client.close()

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


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


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

    def test_loads_all_env_vars(self) -> None:
        env = {
            "MONGOLCHAT_ENDPOINT": "https://api.mongolchat.test",
            "MONGOLCHAT_API_KEY": "env-api-key",
            "MONGOLCHAT_WORKER_KEY": "env-worker-key",
            "MONGOLCHAT_APP_SECRET": "env-secret",
            "MONGOLCHAT_BRANCH_NO": "002",
        }
        with patch.dict(os.environ, env, clear=False):
            cfg = load_config_from_env()

        assert cfg.endpoint == "https://api.mongolchat.test"
        assert cfg.api_key == "env-api-key"
        assert cfg.worker_key == "env-worker-key"
        assert cfg.app_secret == "env-secret"
        assert cfg.branch_no == "002"

    def test_missing_endpoint_raises(self) -> None:
        env = {
            "MONGOLCHAT_API_KEY": "key",
            "MONGOLCHAT_WORKER_KEY": "wk",
            "MONGOLCHAT_APP_SECRET": "secret",
            "MONGOLCHAT_BRANCH_NO": "001",
        }
        with patch.dict(os.environ, env, clear=False):
            os.environ.pop("MONGOLCHAT_ENDPOINT", None)
            with pytest.raises(ValueError, match="MONGOLCHAT_ENDPOINT"):
                load_config_from_env()

    def test_missing_api_key_raises(self) -> None:
        env = {
            "MONGOLCHAT_ENDPOINT": "https://api.mongolchat.test",
            "MONGOLCHAT_WORKER_KEY": "wk",
            "MONGOLCHAT_APP_SECRET": "secret",
            "MONGOLCHAT_BRANCH_NO": "001",
        }
        with patch.dict(os.environ, env, clear=False):
            os.environ.pop("MONGOLCHAT_API_KEY", None)
            with pytest.raises(ValueError, match="MONGOLCHAT_API_KEY"):
                load_config_from_env()

    def test_missing_worker_key_raises(self) -> None:
        env = {
            "MONGOLCHAT_ENDPOINT": "https://api.mongolchat.test",
            "MONGOLCHAT_API_KEY": "key",
            "MONGOLCHAT_APP_SECRET": "secret",
            "MONGOLCHAT_BRANCH_NO": "001",
        }
        with patch.dict(os.environ, env, clear=False):
            os.environ.pop("MONGOLCHAT_WORKER_KEY", None)
            with pytest.raises(ValueError, match="MONGOLCHAT_WORKER_KEY"):
                load_config_from_env()

    def test_missing_app_secret_raises(self) -> None:
        env = {
            "MONGOLCHAT_ENDPOINT": "https://api.mongolchat.test",
            "MONGOLCHAT_API_KEY": "key",
            "MONGOLCHAT_WORKER_KEY": "wk",
            "MONGOLCHAT_BRANCH_NO": "001",
        }
        with patch.dict(os.environ, env, clear=False):
            os.environ.pop("MONGOLCHAT_APP_SECRET", None)
            with pytest.raises(ValueError, match="MONGOLCHAT_APP_SECRET"):
                load_config_from_env()

    def test_missing_branch_no_raises(self) -> None:
        env = {
            "MONGOLCHAT_ENDPOINT": "https://api.mongolchat.test",
            "MONGOLCHAT_API_KEY": "key",
            "MONGOLCHAT_WORKER_KEY": "wk",
            "MONGOLCHAT_APP_SECRET": "secret",
        }
        with patch.dict(os.environ, env, clear=False):
            os.environ.pop("MONGOLCHAT_BRANCH_NO", None)
            with pytest.raises(ValueError, match="MONGOLCHAT_BRANCH_NO"):
                load_config_from_env()


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


class TestMongolChatError:
    """Tests for MongolChatError."""

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

    def test_error_with_code(self) -> None:
        err = MongolChatError("bad request", code=2001)
        assert err.code == 2001

    def test_error_with_response(self) -> None:
        body = {"error": "details"}
        err = MongolChatError("api error", code=3001, response=body)
        assert err.code == 3001
        assert err.response == body

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