"""Tests for the FastAPI webhook endpoint and dispatch handlers."""

from __future__ import annotations

import asyncio
import json
from pathlib import Path
from unittest.mock import AsyncMock, patch

from helpers import sign_payload


async def test_health_check(async_client):
    resp = await async_client.get("/health")
    assert resp.status_code == 200
    assert resp.json() == {"status": "ok"}


async def test_webhook_invalid_signature(async_client, pr_payload):
    body = json.dumps(pr_payload).encode()
    resp = await async_client.post(
        "/webhook",
        content=body,
        headers={
            "Content-Type": "application/json",
            "X-GitHub-Event": "pull_request",
            "X-Hub-Signature-256": "sha256=invalid",
            "X-GitHub-Delivery": "delivery-1",
        },
    )
    assert resp.status_code == 401


async def test_webhook_unknown_event(async_client):
    payload = {"action": "test"}
    body = json.dumps(payload).encode()
    resp = await async_client.post(
        "/webhook",
        content=body,
        headers={
            "Content-Type": "application/json",
            "X-GitHub-Event": "unknown_event",
            "X-Hub-Signature-256": sign_payload(body),
            "X-GitHub-Delivery": "delivery-2",
        },
    )
    assert resp.status_code == 200
    assert resp.json()["status"] == "ignored"


async def test_webhook_accepted(async_client, issue_payload, monkeypatch):
    dispatched = []

    async def fake_dispatch(payload, **kwargs):
        dispatched.append(payload["action"])

    monkeypatch.setattr(
        "github_app.app.EVENT_HANDLERS",
        {"issues": fake_dispatch},
    )

    body = json.dumps(issue_payload).encode()
    resp = await async_client.post(
        "/webhook",
        content=body,
        headers={
            "Content-Type": "application/json",
            "X-GitHub-Event": "issues",
            "X-Hub-Signature-256": sign_payload(body),
            "X-GitHub-Delivery": "delivery-3",
        },
    )
    assert resp.status_code == 200
    assert resp.json()["status"] == "accepted"

    await asyncio.sleep(0.05)
    assert dispatched == ["opened"]


async def test_webhook_task_tracking(async_client, issue_payload, monkeypatch):
    """Background tasks are tracked in running_tasks and cleaned up."""
    from github_app.app import app

    gate = asyncio.Event()

    async def slow_handler(payload, **kwargs):
        await gate.wait()

    monkeypatch.setattr(
        "github_app.app.EVENT_HANDLERS",
        {"issues": slow_handler},
    )

    body = json.dumps(issue_payload).encode()
    await async_client.post(
        "/webhook",
        content=body,
        headers={
            "Content-Type": "application/json",
            "X-GitHub-Event": "issues",
            "X-Hub-Signature-256": sign_payload(body),
            "X-GitHub-Delivery": "delivery-4",
        },
    )

    await asyncio.sleep(0.01)
    assert len(app.state.running_tasks) == 1

    gate.set()
    await asyncio.sleep(0.05)
    assert len(app.state.running_tasks) == 0


async def test_webhook_duplicate_delivery(async_client, issue_payload, monkeypatch):
    """Duplicate delivery IDs are detected and skipped."""
    dispatched = []

    async def fake_dispatch(payload, **kwargs):
        dispatched.append(payload["action"])

    monkeypatch.setattr(
        "github_app.app.EVENT_HANDLERS",
        {"issues": fake_dispatch},
    )

    body = json.dumps(issue_payload).encode()
    headers = {
        "Content-Type": "application/json",
        "X-GitHub-Event": "issues",
        "X-Hub-Signature-256": sign_payload(body),
        "X-GitHub-Delivery": "delivery-dup-test",
    }

    resp1 = await async_client.post("/webhook", content=body, headers=headers)
    assert resp1.json()["status"] == "accepted"

    await asyncio.sleep(0.05)

    resp2 = await async_client.post("/webhook", content=body, headers=headers)
    assert resp2.json()["status"] == "duplicate"
    assert resp2.json()["delivery"] == "delivery-dup-test"

    assert len(dispatched) == 1


async def test_dispatch_issues_writes_context(mock_config, issue_payload):
    """dispatch_issues writes ci-context.json and calls run_agent."""
    from github_app.app import dispatch_issues

    http_client = AsyncMock()

    with (
        patch("github_app.app.get_installation_token", return_value="tok"),
        patch(
            "github_app.app.clone_repo",
            return_value=Path("/tmp/repo"),
        ),
        patch("github_app.app._write_ci_context") as write_ctx_mock,
        patch("github_app.app.run_agent") as run_agent_mock,
    ):
        run_agent_mock.return_value = "done"
        await dispatch_issues(
            issue_payload,
            config=mock_config,
            http_client=http_client,
        )

    write_ctx_mock.assert_called_once()
    ctx_arg = write_ctx_mock.call_args.args[1]
    assert ctx_arg["event_type"] == "issues"
    assert ctx_arg["action"] == "opened"
    assert ctx_arg["number"] == 7
    assert ctx_arg["owner"] == "test-owner"
    assert ctx_arg["repo"] == "test-repo"

    run_agent_mock.assert_called_once_with(
        mock_config,
        Path("/tmp/repo"),
        "tok",
    )


async def test_dispatch_pr_writes_context(mock_config, pr_payload):
    """dispatch_pr writes ci-context.json with PR fields."""
    from github_app.app import dispatch_pr

    http_client = AsyncMock()

    with (
        patch("github_app.app.get_installation_token", return_value="tok"),
        patch(
            "github_app.app.clone_repo",
            return_value=Path("/tmp/repo"),
        ),
        patch("github_app.app._write_ci_context") as write_ctx_mock,
        patch("github_app.app.run_agent") as run_agent_mock,
    ):
        run_agent_mock.return_value = "done"
        await dispatch_pr(
            pr_payload,
            config=mock_config,
            http_client=http_client,
        )

    write_ctx_mock.assert_called_once()
    ctx_arg = write_ctx_mock.call_args.args[1]
    assert ctx_arg["event_type"] == "pull_request"
    assert ctx_arg["action"] == "opened"
    assert ctx_arg["number"] == 42
    assert ctx_arg["base_ref"] == "main"
    assert ctx_arg["head_ref"] == "feature-branch"

    run_agent_mock.assert_called_once()
    call_kwargs = run_agent_mock.call_args.kwargs
    assert call_kwargs["agent"] == "codeflash-deep"
    assert "CI run triggered by PR #42" in call_kwargs["prompt"]
    assert "feature-branch" in call_kwargs["prompt"]


async def test_dispatch_push_writes_context(mock_config, push_payload):
    """dispatch_push writes ci-context.json with push fields."""
    from github_app.app import dispatch_push

    http_client = AsyncMock()

    with (
        patch("github_app.app.get_installation_token", return_value="tok"),
        patch(
            "github_app.app.clone_repo",
            return_value=Path("/tmp/repo"),
        ),
        patch("github_app.app._write_ci_context") as write_ctx_mock,
        patch("github_app.app.run_agent") as run_agent_mock,
    ):
        run_agent_mock.return_value = "done"
        await dispatch_push(
            push_payload,
            config=mock_config,
            http_client=http_client,
        )

    write_ctx_mock.assert_called_once()
    ctx_arg = write_ctx_mock.call_args.args[1]
    assert ctx_arg["event_type"] == "push"
    assert ctx_arg["action"] is None
    assert ctx_arg["head_sha"] == "abc123"
    assert ctx_arg["ref"] == "refs/heads/main"

    run_agent_mock.assert_called_once()


async def test_dispatch_push_ignores_non_default_branch(mock_config, push_payload):
    """Push to a non-default branch is ignored."""
    from github_app.app import dispatch_push

    push_payload["ref"] = "refs/heads/feature-branch"
    http_client = AsyncMock()

    with (
        patch("github_app.app.get_installation_token") as token_mock,
        patch("github_app.app.run_agent") as run_agent_mock,
    ):
        await dispatch_push(
            push_payload,
            config=mock_config,
            http_client=http_client,
        )

    token_mock.assert_not_called()
    run_agent_mock.assert_not_called()


async def test_dispatch_issues_ignores_irrelevant_action(mock_config, issue_payload):
    """Actions other than opened/labeled are ignored."""
    from github_app.app import dispatch_issues

    issue_payload["action"] = "closed"
    http_client = AsyncMock()

    with patch("github_app.app.run_agent") as run_agent_mock:
        await dispatch_issues(
            issue_payload,
            config=mock_config,
            http_client=http_client,
        )

    run_agent_mock.assert_not_called()


async def test_dispatch_pr_ignores_irrelevant_action(mock_config, pr_payload):
    """Actions other than opened/synchronize are ignored."""
    from github_app.app import dispatch_pr

    pr_payload["action"] = "closed"
    http_client = AsyncMock()

    with patch("github_app.app.run_agent") as run_agent_mock:
        await dispatch_pr(
            pr_payload,
            config=mock_config,
            http_client=http_client,
        )

    run_agent_mock.assert_not_called()


async def test_write_ci_context(tmp_path):
    """_write_ci_context creates the .codeflash dir and writes JSON."""
    from github_app.app import _write_ci_context

    _write_ci_context(str(tmp_path), {"event_type": "issues", "number": 7})

    ctx_file = tmp_path / ".codeflash" / "ci-context.json"
    assert ctx_file.exists()
    data = json.loads(ctx_file.read_text())
    assert data["event_type"] == "issues"
    assert data["number"] == 7
