from __future__ import annotations
import socket
import pytest
from httpx import AsyncClient, ASGITransport
from project_hub.web.app import create_app, _find_free_port


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def stub_templates(tmp_path):
    """Create minimal stub templates for testing routes."""
    tpl_dir = tmp_path / "templates"
    tpl_dir.mkdir()
    for name in [
        "loading.html", "dashboard.html", "dashboard_fragment.html",
        "mrs.html", "mrs_fragment.html", "vulns.html", "vulns_fragment.html",
        "pipelines.html", "pipelines_fragment.html",
        "sync_status_fragment.html", "events_panel_fragment.html",
        "settings.html", "toast.html",
    ]:
        content = (
            "<html>loading</html>"
            if "loading" in name
            else f"<html>{{{{ state }}}}</html>"
        )
        (tpl_dir / name).write_text(content)
    static_dir = tmp_path / "static"
    static_dir.mkdir()
    return tpl_dir, static_dir


@pytest.fixture
def fake_state():
    return {
        "repos": [{"name": "my-awesome-repo", "branch": "main"}],
        "mrs": [],
        "vulns": [],
        "pipelines": [],
        "events": [],
        "sync_status": "idle",
    }


# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------

def make_client(app) -> AsyncClient:
    return AsyncClient(transport=ASGITransport(app=app), base_url="http://test")


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_dashboard_loading_state(stub_templates):
    tpl_dir, static_dir = stub_templates
    app = create_app(
        get_state=lambda: None,
        _template_dir=tpl_dir,
        _static_dir=static_dir,
    )
    async with make_client(app) as client:
        resp = await client.get("/")
    assert resp.status_code == 200
    assert resp.headers["content-type"].startswith("text/html")
    # get_state returns None -> loading template is rendered (not dashboard)
    assert "<html>loading</html>" == resp.text


@pytest.mark.asyncio
async def test_dashboard_with_data(stub_templates, fake_state):
    tpl_dir, static_dir = stub_templates
    # Make dashboard.html actually render something identifiable
    (tpl_dir / "dashboard.html").write_text(
        "{% for repo in state['repos'] %}<span>{{ repo['name'] }}</span>{% endfor %}"
    )
    app = create_app(
        get_state=lambda: fake_state,
        _template_dir=tpl_dir,
        _static_dir=static_dir,
    )
    async with make_client(app) as client:
        resp = await client.get("/")
    assert resp.status_code == 200
    assert "my-awesome-repo" in resp.text


@pytest.mark.asyncio
async def test_dashboard_fragment(stub_templates):
    tpl_dir, static_dir = stub_templates
    app = create_app(
        get_state=lambda: {"repos": []},
        _template_dir=tpl_dir,
        _static_dir=static_dir,
    )
    async with make_client(app) as client:
        resp = await client.get("/fragment/dashboard")
    assert resp.status_code == 200
    assert "<html>" in resp.text


@pytest.mark.asyncio
async def test_mrs_page(stub_templates):
    tpl_dir, static_dir = stub_templates
    app = create_app(
        get_state=lambda: {"mrs": []},
        _template_dir=tpl_dir,
        _static_dir=static_dir,
    )
    async with make_client(app) as client:
        resp = await client.get("/mrs")
    assert resp.status_code == 200


@pytest.mark.asyncio
async def test_action_sync_triggers_callback(stub_templates):
    tpl_dir, static_dir = stub_templates
    called = []

    async def mock_refresh():
        called.append(True)

    app = create_app(
        get_state=lambda: {},
        trigger_refresh=mock_refresh,
        _template_dir=tpl_dir,
        _static_dir=static_dir,
    )
    async with make_client(app) as client:
        resp = await client.post("/action/sync", headers={"HX-Request": "true"})
    assert resp.status_code == 204
    assert called == [True]


def test_find_free_port():
    """Listen on a port, then verify _find_free_port skips it."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind(("127.0.0.1", 0))
        s.listen(1)
        occupied = s.getsockname()[1]
        result = _find_free_port(occupied)
    assert result != occupied
    assert result > occupied


@pytest.mark.asyncio
async def test_dismiss_event_calls_callback(stub_templates):
    tpl_dir, static_dir = stub_templates
    dismissed = []

    async def mock_dismiss(event_id: int):
        dismissed.append(event_id)

    app = create_app(
        get_state=lambda: {},
        dismiss_event=mock_dismiss,
        _template_dir=tpl_dir,
        _static_dir=static_dir,
    )
    async with make_client(app) as client:
        resp = await client.post("/action/dismiss-event/42", headers={"HX-Request": "true"})
    assert resp.status_code == 200
    assert dismissed == [42]


@pytest.mark.asyncio
async def test_dismiss_event_no_callback(stub_templates):
    """Dismiss endpoint works gracefully when no callback is configured."""
    tpl_dir, static_dir = stub_templates
    app = create_app(
        get_state=lambda: {},
        _template_dir=tpl_dir,
        _static_dir=static_dir,
    )
    async with make_client(app) as client:
        resp = await client.post("/action/dismiss-event/7", headers={"HX-Request": "true"})
    assert resp.status_code == 200


def test_find_free_port_default():
    """When the starting port is free, it should be returned as-is."""
    # Find a free port first
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(("127.0.0.1", 0))
        free_port = s.getsockname()[1]
    # Port is now released; _find_free_port should return it
    result = _find_free_port(free_port)
    assert result == free_port
