"""Tests for gateway.platforms.feishu — Feishu scan-to-create registration."""

import json
from unittest.mock import patch, MagicMock
import pytest


def _mock_urlopen(response_data, status=200):
    """Create a mock for urllib.request.urlopen that returns JSON response_data."""
    mock_response = MagicMock()
    mock_response.read.return_value = json.dumps(response_data).encode("utf-8")
    mock_response.status = status
    mock_response.__enter__ = lambda s: s
    mock_response.__exit__ = MagicMock(return_value=False)
    return mock_response


class TestPostRegistration:
    """Tests for the low-level HTTP helper."""

    @patch("gateway.platforms.feishu.urlopen")
    def test_post_registration_returns_parsed_json(self, mock_urlopen_fn):
        from gateway.platforms.feishu import _post_registration

        mock_urlopen_fn.return_value = _mock_urlopen({"nonce": "abc", "supported_auth_methods": ["client_secret"]})
        result = _post_registration("https://accounts.feishu.cn", {"action": "init"})
        assert result["nonce"] == "abc"
        assert "client_secret" in result["supported_auth_methods"]

    @patch("gateway.platforms.feishu.urlopen")
    def test_post_registration_sends_form_encoded_body(self, mock_urlopen_fn):
        from gateway.platforms.feishu import _post_registration

        mock_urlopen_fn.return_value = _mock_urlopen({})
        _post_registration("https://accounts.feishu.cn", {"action": "init", "key": "val"})
        call_args = mock_urlopen_fn.call_args
        request = call_args[0][0]
        body = request.data.decode("utf-8")
        assert "action=init" in body
        assert "key=val" in body
        assert request.get_header("Content-type") == "application/x-www-form-urlencoded"


class TestInitRegistration:
    """Tests for the init step."""

    @patch("gateway.platforms.feishu.urlopen")
    def test_init_succeeds_when_client_secret_supported(self, mock_urlopen_fn):
        from gateway.platforms.feishu import _init_registration

        mock_urlopen_fn.return_value = _mock_urlopen({
            "nonce": "abc",
            "supported_auth_methods": ["client_secret"],
        })
        _init_registration("feishu")

    @patch("gateway.platforms.feishu.urlopen")
    def test_init_raises_when_client_secret_not_supported(self, mock_urlopen_fn):
        from gateway.platforms.feishu import _init_registration

        mock_urlopen_fn.return_value = _mock_urlopen({
            "nonce": "abc",
            "supported_auth_methods": ["other_method"],
        })
        with pytest.raises(RuntimeError, match="client_secret"):
            _init_registration("feishu")

    @patch("gateway.platforms.feishu.urlopen")
    def test_init_uses_lark_url_for_lark_domain(self, mock_urlopen_fn):
        from gateway.platforms.feishu import _init_registration

        mock_urlopen_fn.return_value = _mock_urlopen({
            "nonce": "abc",
            "supported_auth_methods": ["client_secret"],
        })
        _init_registration("lark")
        call_args = mock_urlopen_fn.call_args
        request = call_args[0][0]
        assert "larksuite.com" in request.full_url


class TestBeginRegistration:
    """Tests for the begin step."""

    @patch("gateway.platforms.feishu.urlopen")
    def test_begin_returns_device_code_and_qr_url(self, mock_urlopen_fn):
        from gateway.platforms.feishu import _begin_registration

        mock_urlopen_fn.return_value = _mock_urlopen({
            "device_code": "dc_123",
            "verification_uri_complete": "https://accounts.feishu.cn/qr/abc",
            "user_code": "ABCD-1234",
            "interval": 5,
            "expire_in": 600,
        })
        result = _begin_registration("feishu")
        assert result["device_code"] == "dc_123"
        assert "qr_url" in result
        assert "accounts.feishu.cn" in result["qr_url"]
        assert result["user_code"] == "ABCD-1234"
        assert result["interval"] == 5
        assert result["expire_in"] == 600

    @patch("gateway.platforms.feishu.urlopen")
    def test_begin_sends_correct_archetype(self, mock_urlopen_fn):
        from gateway.platforms.feishu import _begin_registration

        mock_urlopen_fn.return_value = _mock_urlopen({
            "device_code": "dc_123",
            "verification_uri_complete": "https://example.com/qr",
            "user_code": "X",
            "interval": 5,
            "expire_in": 600,
        })
        _begin_registration("feishu")
        request = mock_urlopen_fn.call_args[0][0]
        body = request.data.decode("utf-8")
        assert "archetype=PersonalAgent" in body
        assert "auth_method=client_secret" in body


class TestPollRegistration:
    """Tests for the poll step."""

    @patch("gateway.platforms.feishu.time")
    @patch("gateway.platforms.feishu.urlopen")
    def test_poll_returns_credentials_on_success(self, mock_urlopen_fn, mock_time):
        from gateway.platforms.feishu import _poll_registration

        mock_time.monotonic.side_effect = [0, 1]
        mock_time.sleep = MagicMock()

        mock_urlopen_fn.return_value = _mock_urlopen({
            "client_id": "cli_app123",
            "client_secret": "secret456",
            "user_info": {"open_id": "ou_owner", "tenant_brand": "feishu"},
        })
        result = _poll_registration(
            device_code="dc_123", interval=1, expire_in=60, domain="feishu"
        )
        assert result is not None
        assert result["app_id"] == "cli_app123"
        assert result["app_secret"] == "secret456"
        assert result["domain"] == "feishu"
        assert result["open_id"] == "ou_owner"

    @patch("gateway.platforms.feishu.time")
    @patch("gateway.platforms.feishu.urlopen")
    def test_poll_switches_domain_on_lark_tenant_brand(self, mock_urlopen_fn, mock_time):
        from gateway.platforms.feishu import _poll_registration

        mock_time.monotonic.side_effect = [0, 1, 2]
        mock_time.sleep = MagicMock()

        pending_resp = _mock_urlopen({
            "error": "authorization_pending",
            "user_info": {"tenant_brand": "lark"},
        })
        success_resp = _mock_urlopen({
            "client_id": "cli_lark",
            "client_secret": "secret_lark",
            "user_info": {"open_id": "ou_lark", "tenant_brand": "lark"},
        })
        mock_urlopen_fn.side_effect = [pending_resp, success_resp]

        result = _poll_registration(
            device_code="dc_123", interval=0, expire_in=60, domain="feishu"
        )
        assert result is not None
        assert result["domain"] == "lark"

    @patch("gateway.platforms.feishu.time")
    @patch("gateway.platforms.feishu.urlopen")
    def test_poll_success_with_lark_brand_in_same_response(self, mock_urlopen_fn, mock_time):
        """Credentials and lark tenant_brand in one response must not be discarded."""
        from gateway.platforms.feishu import _poll_registration

        mock_time.monotonic.side_effect = [0, 1]
        mock_time.sleep = MagicMock()

        mock_urlopen_fn.return_value = _mock_urlopen({
            "client_id": "cli_lark_direct",
            "client_secret": "secret_lark_direct",
            "user_info": {"open_id": "ou_lark_direct", "tenant_brand": "lark"},
        })
        result = _poll_registration(
            device_code="dc_123", interval=1, expire_in=60, domain="feishu"
        )
        assert result is not None
        assert result["app_id"] == "cli_lark_direct"
        assert result["domain"] == "lark"
        assert result["open_id"] == "ou_lark_direct"

    @patch("gateway.platforms.feishu.time")
    @patch("gateway.platforms.feishu.urlopen")
    def test_poll_returns_none_on_access_denied(self, mock_urlopen_fn, mock_time):
        from gateway.platforms.feishu import _poll_registration

        mock_time.monotonic.side_effect = [0, 1]
        mock_time.sleep = MagicMock()

        mock_urlopen_fn.return_value = _mock_urlopen({
            "error": "access_denied",
        })
        result = _poll_registration(
            device_code="dc_123", interval=1, expire_in=60, domain="feishu"
        )
        assert result is None

    @patch("gateway.platforms.feishu.time")
    @patch("gateway.platforms.feishu.urlopen")
    def test_poll_returns_none_on_timeout(self, mock_urlopen_fn, mock_time):
        from gateway.platforms.feishu import _poll_registration

        mock_time.monotonic.side_effect = [0, 999]
        mock_time.sleep = MagicMock()

        mock_urlopen_fn.return_value = _mock_urlopen({
            "error": "authorization_pending",
        })
        result = _poll_registration(
            device_code="dc_123", interval=1, expire_in=1, domain="feishu"
        )
        assert result is None

    @patch("gateway.platforms.feishu.time")
    @patch("gateway.platforms.feishu.urlopen")
    def test_poll_timeout_uses_monotonic_clock(self, mock_urlopen_fn, mock_time):
        from gateway.platforms.feishu import _poll_registration

        mock_time.monotonic.side_effect = [1000, 1000.2, 1001.1]
        mock_time.time.side_effect = [1000, 900, 901, 902]
        mock_time.sleep = MagicMock()

        mock_urlopen_fn.return_value = _mock_urlopen({
            "error": "authorization_pending",
        })
        result = _poll_registration(
            device_code="dc_123", interval=1, expire_in=1, domain="feishu"
        )

        assert result is None
        mock_urlopen_fn.assert_called_once()


class TestRenderQr:
    """Tests for QR code terminal rendering."""

    @patch("gateway.platforms.feishu._qrcode_mod", create=True)
    def test_render_qr_returns_true_on_success(self, mock_qrcode_mod):
        from gateway.platforms.feishu import _render_qr

        mock_qr = MagicMock()
        mock_qrcode_mod.QRCode.return_value = mock_qr
        assert _render_qr("https://example.com/qr") is True
        mock_qr.add_data.assert_called_once_with("https://example.com/qr")
        mock_qr.make.assert_called_once_with(fit=True)
        mock_qr.print_ascii.assert_called_once()

    def test_render_qr_returns_false_when_qrcode_missing(self):
        from gateway.platforms.feishu import _render_qr

        with patch("gateway.platforms.feishu._qrcode_mod", None):
            assert _render_qr("https://example.com/qr") is False


class TestProbeBot:
    """Tests for bot connectivity verification."""

    @patch("gateway.platforms.feishu.FEISHU_AVAILABLE", True)
    def test_probe_returns_bot_info_on_success(self):
        from gateway.platforms.feishu import probe_bot

        with patch("gateway.platforms.feishu._probe_bot_sdk") as mock_sdk:
            mock_sdk.return_value = {"bot_name": "TestBot", "bot_open_id": "ou_bot123"}
            result = probe_bot("cli_app", "secret", "feishu")

        assert result is not None
        assert result["bot_name"] == "TestBot"
        assert result["bot_open_id"] == "ou_bot123"

    @patch("gateway.platforms.feishu.FEISHU_AVAILABLE", True)
    def test_probe_returns_none_on_failure(self):
        from gateway.platforms.feishu import probe_bot

        with patch("gateway.platforms.feishu._probe_bot_sdk") as mock_sdk:
            mock_sdk.return_value = None
            result = probe_bot("bad_id", "bad_secret", "feishu")

        assert result is None

    @patch("gateway.platforms.feishu.FEISHU_AVAILABLE", False)
    @patch("gateway.platforms.feishu.urlopen")
    def test_http_fallback_when_sdk_unavailable(self, mock_urlopen_fn):
        """Without lark_oapi, probe falls back to raw HTTP."""
        from gateway.platforms.feishu import probe_bot

        token_resp = _mock_urlopen({"code": 0, "tenant_access_token": "t-123"})
        bot_resp = _mock_urlopen({"code": 0, "bot": {"bot_name": "HttpBot", "open_id": "ou_http"}})
        mock_urlopen_fn.side_effect = [token_resp, bot_resp]

        result = probe_bot("cli_app", "secret", "feishu")
        assert result is not None
        assert result["bot_name"] == "HttpBot"

    @patch("gateway.platforms.feishu.FEISHU_AVAILABLE", False)
    @patch("gateway.platforms.feishu.urlopen")
    def test_http_fallback_returns_none_on_network_error(self, mock_urlopen_fn):
        from gateway.platforms.feishu import probe_bot
        from urllib.error import URLError

        mock_urlopen_fn.side_effect = URLError("connection refused")
        result = probe_bot("cli_app", "secret", "feishu")
        assert result is None


class TestQrRegister:
    """Tests for the public qr_register entry point."""

    @patch("gateway.platforms.feishu.probe_bot")
    @patch("gateway.platforms.feishu._render_qr")
    @patch("gateway.platforms.feishu._poll_registration")
    @patch("gateway.platforms.feishu._begin_registration")
    @patch("gateway.platforms.feishu._init_registration")
    def test_qr_register_success_flow(
        self, mock_init, mock_begin, mock_poll, mock_render, mock_probe
    ):
        from gateway.platforms.feishu import qr_register

        mock_begin.return_value = {
            "device_code": "dc_123",
            "qr_url": "https://example.com/qr",
            "user_code": "ABCD",
            "interval": 1,
            "expire_in": 60,
        }
        mock_poll.return_value = {
            "app_id": "cli_app",
            "app_secret": "secret",
            "domain": "feishu",
            "open_id": "ou_owner",
        }
        mock_probe.return_value = {"bot_name": "MyBot", "bot_open_id": "ou_bot"}

        result = qr_register()
        assert result is not None
        assert result["app_id"] == "cli_app"
        assert result["app_secret"] == "secret"
        assert result["bot_name"] == "MyBot"
        mock_init.assert_called_once()
        mock_render.assert_called_once()

    @patch("gateway.platforms.feishu._init_registration")
    def test_qr_register_returns_none_on_init_failure(self, mock_init):
        from gateway.platforms.feishu import qr_register

        mock_init.side_effect = RuntimeError("not supported")
        result = qr_register()
        assert result is None

    @patch("gateway.platforms.feishu._render_qr")
    @patch("gateway.platforms.feishu._poll_registration")
    @patch("gateway.platforms.feishu._begin_registration")
    @patch("gateway.platforms.feishu._init_registration")
    def test_qr_register_returns_none_on_poll_failure(
        self, mock_init, mock_begin, mock_poll, mock_render
    ):
        from gateway.platforms.feishu import qr_register

        mock_begin.return_value = {
            "device_code": "dc_123",
            "qr_url": "https://example.com/qr",
            "user_code": "ABCD",
            "interval": 1,
            "expire_in": 60,
        }
        mock_poll.return_value = None

        result = qr_register()
        assert result is None

    # -- Contract: expected errors → None, unexpected errors → propagate --

    @patch("gateway.platforms.feishu._init_registration")
    def test_qr_register_returns_none_on_network_error(self, mock_init):
        """URLError (network down) is an expected failure → None."""
        from gateway.platforms.feishu import qr_register
        from urllib.error import URLError

        mock_init.side_effect = URLError("DNS resolution failed")
        result = qr_register()
        assert result is None

    @patch("gateway.platforms.feishu._init_registration")
    def test_qr_register_returns_none_on_json_error(self, mock_init):
        """Malformed server response is an expected failure → None."""
        from gateway.platforms.feishu import qr_register

        mock_init.side_effect = json.JSONDecodeError("bad json", "", 0)
        result = qr_register()
        assert result is None

    @patch("gateway.platforms.feishu._init_registration")
    def test_qr_register_propagates_unexpected_errors(self, mock_init):
        """Bugs (e.g. AttributeError) must not be swallowed — they propagate."""
        from gateway.platforms.feishu import qr_register

        mock_init.side_effect = AttributeError("some internal bug")
        with pytest.raises(AttributeError, match="some internal bug"):
            qr_register()

    # -- Negative paths: partial/malformed server responses --

    @patch("gateway.platforms.feishu._render_qr")
    @patch("gateway.platforms.feishu._begin_registration")
    @patch("gateway.platforms.feishu._init_registration")
    def test_qr_register_returns_none_when_begin_missing_device_code(
        self, mock_init, mock_begin, mock_render
    ):
        """Server returns begin response without device_code → RuntimeError → None."""
        from gateway.platforms.feishu import qr_register

        mock_begin.side_effect = RuntimeError("Feishu registration did not return a device_code")
        result = qr_register()
        assert result is None

    @patch("gateway.platforms.feishu.probe_bot")
    @patch("gateway.platforms.feishu._render_qr")
    @patch("gateway.platforms.feishu._poll_registration")
    @patch("gateway.platforms.feishu._begin_registration")
    @patch("gateway.platforms.feishu._init_registration")
    def test_qr_register_succeeds_even_when_probe_fails(
        self, mock_init, mock_begin, mock_poll, mock_render, mock_probe
    ):
        """Registration succeeds but probe fails → result with bot_name=None."""
        from gateway.platforms.feishu import qr_register

        mock_begin.return_value = {
            "device_code": "dc_123",
            "qr_url": "https://example.com/qr",
            "user_code": "ABCD",
            "interval": 1,
            "expire_in": 60,
        }
        mock_poll.return_value = {
            "app_id": "cli_app",
            "app_secret": "secret",
            "domain": "feishu",
            "open_id": "ou_owner",
        }
        mock_probe.return_value = None  # probe failed

        result = qr_register()
        assert result is not None
        assert result["app_id"] == "cli_app"
        assert result["bot_name"] is None
        assert result["bot_open_id"] is None
