"""Tests for the login command."""

import threading
from unittest.mock import patch
from urllib.request import urlopen

import pytest

from skilark_cli.commands.login import run
from skilark_cli.config_store import ConfigStore


def test_run_success(tmp_path):
    """Login flow writes user_id to config on successful callback."""
    config_store = ConfigStore(config_dir=tmp_path)
    api_url = "http://127.0.0.1:9999"  # Doesn't matter — browser.open is mocked

    def simulate_callback(login_fn):
        """Run login in a thread, then make the callback request."""
        # We need to intercept the port from the login URL that webbrowser.open receives
        captured = {}

        def fake_open(url):
            # Extract the channel_id (127.0.0.1:PORT) from the URL
            import re
            match = re.search(r"channel_id=127\.0\.0\.1%3A(\d+)", url)
            if not match:
                match = re.search(r"channel_id=127\.0\.0\.1:(\d+)", url)
            if match:
                captured["port"] = int(match.group(1))

        with patch("webbrowser.open", side_effect=fake_open):
            # Start login in a thread
            t = threading.Thread(target=login_fn)
            t.start()

            # Wait for the server to start and webbrowser.open to be called
            import time
            for _ in range(50):
                if "port" in captured:
                    break
                time.sleep(0.05)

            assert "port" in captured, "webbrowser.open was not called with expected URL"

            # Simulate the OAuth redirect callback
            port = captured["port"]
            urlopen(f"http://127.0.0.1:{port}/callback?user_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890&status=ok")

            t.join(timeout=5)

    simulate_callback(lambda: run(config_store=config_store, api_url=api_url))

    assert not config_store.is_first_run()
    config = config_store.load()
    assert config["user_id"] == "a1b2c3d4-e5f6-7890-abcd-ef1234567890"


def test_run_success_after_favicon(tmp_path):
    """Login succeeds even when browser sends /favicon.ico before /callback."""
    config_store = ConfigStore(config_dir=tmp_path)
    captured = {}

    def fake_open(url):
        import re
        match = re.search(r"channel_id=127\.0\.0\.1%3A(\d+)", url)
        if not match:
            match = re.search(r"channel_id=127\.0\.0\.1:(\d+)", url)
        if match:
            captured["port"] = int(match.group(1))

    with patch("webbrowser.open", side_effect=fake_open):
        t = threading.Thread(
            target=run,
            kwargs={"config_store": config_store, "api_url": "http://127.0.0.1:9999"},
        )
        t.start()

        import time
        for _ in range(50):
            if "port" in captured:
                break
            time.sleep(0.05)

        port = captured["port"]
        # Browser fires favicon first
        try:
            urlopen(f"http://127.0.0.1:{port}/favicon.ico")
        except Exception:
            pass  # 404 is expected
        # Then the real callback arrives
        urlopen(f"http://127.0.0.1:{port}/callback?user_id=b2c3d4e5-f6a7-8901-bcde-f12345678901&status=ok")
        t.join(timeout=5)

    config = config_store.load()
    assert config["user_id"] == "b2c3d4e5-f6a7-8901-bcde-f12345678901"


def test_run_updates_existing_config(tmp_path):
    """Login preserves existing config fields, only updates user_id."""
    config_store = ConfigStore(config_dir=tmp_path)
    config_store.save(user_id="e5f6a7b8-c9d0-1234-efab-345678901234", topics=["python", "go"], api_url="https://api.skilark.com")

    captured = {}

    def fake_open(url):
        import re
        match = re.search(r"channel_id=127\.0\.0\.1%3A(\d+)", url)
        if not match:
            match = re.search(r"channel_id=127\.0\.0\.1:(\d+)", url)
        if match:
            captured["port"] = int(match.group(1))

    with patch("webbrowser.open", side_effect=fake_open):
        t = threading.Thread(
            target=run,
            kwargs={"config_store": config_store, "api_url": "https://api.skilark.com"},
        )
        t.start()

        import time
        for _ in range(50):
            if "port" in captured:
                break
            time.sleep(0.05)

        port = captured["port"]
        urlopen(f"http://127.0.0.1:{port}/callback?user_id=c3d4e5f6-a7b8-9012-cdef-123456789012&status=ok")
        t.join(timeout=5)

    config = config_store.load()
    assert config["user_id"] == "c3d4e5f6-a7b8-9012-cdef-123456789012"
    assert config["topics"] == ["python", "go"]  # preserved


def test_run_timeout(tmp_path, capsys):
    """Login prints error when server times out with no callback."""
    config_store = ConfigStore(config_dir=tmp_path)

    # Mock monotonic to fast-forward past the 120s deadline
    call_count = 0

    def fast_monotonic():
        nonlocal call_count
        call_count += 1
        # First call sets the deadline, subsequent calls exceed it
        return 1000.0 if call_count <= 1 else 1200.0

    with patch("webbrowser.open"), \
         patch("http.server.HTTPServer.handle_request"), \
         patch("skilark_cli.commands.login.time") as mock_time:
        mock_time.monotonic = fast_monotonic
        run(config_store=config_store, api_url="http://127.0.0.1:9999")

    captured = capsys.readouterr()
    assert "failed or timed out" in captured.out


def test_update_user_id(tmp_path):
    """ConfigStore.update_user_id replaces user_id, preserves other fields."""
    config_store = ConfigStore(config_dir=tmp_path)
    config_store.save(user_id="old", topics=["rust"], api_url="https://api.skilark.com")
    config_store.update_user_id("new")

    config = config_store.load()
    assert config["user_id"] == "new"
    assert config["topics"] == ["rust"]
    assert config["api_url"] == "https://api.skilark.com"


def test_dispatch_first_run(tmp_path):
    """dispatch() on first run creates config with user_id from callback."""
    captured = {}

    def fake_open(url):
        import re
        match = re.search(r"channel_id=127\.0\.0\.1%3A(\d+)", url)
        if not match:
            match = re.search(r"channel_id=127\.0\.0\.1:(\d+)", url)
        if match:
            captured["port"] = int(match.group(1))

    config_store = ConfigStore(config_dir=tmp_path)

    with patch("webbrowser.open", side_effect=fake_open):
        t = threading.Thread(
            target=run,
            kwargs={"config_store": config_store, "api_url": "https://api.skilark.com"},
        )
        t.start()

        import time
        for _ in range(50):
            if "port" in captured:
                break
            time.sleep(0.05)

        port = captured["port"]
        urlopen(f"http://127.0.0.1:{port}/callback?user_id=d4e5f6a7-b8c9-0123-defa-234567890123&status=ok")
        t.join(timeout=5)

    assert not config_store.is_first_run()
    config = config_store.load()
    assert config["user_id"] == "d4e5f6a7-b8c9-0123-defa-234567890123"
