# retoor <retoor@molodetz.nl>

import asyncio
import pytest
from devplacepy.database import (
    add_issue_usage,
    get_issue_usage,
    get_table,
    init_db,
    refresh_snapshot,
    set_setting,
)
from devplacepy.services.gitea import runtime, store
from devplacepy.services.gitea.config import gitea_config
from devplacepy.services.gitea.enhance import enhance_ticket
from devplacepy.services.gitea.fake import FakeGiteaClient
from devplacepy.services.gitea.planning import (
    collect_open_issues,
    generate_plan,
    verbatim_tickets,
)
from devplacepy.services.jobs.planning_service import PlanningReportService
from devplacepy.services.gitea.service import IssueTrackerService
from devplacepy.services.jobs import queue
from devplacepy.services.jobs.issue_create_service import IssueCreateService
from devplacepy.services.openai_gateway.usage import new_usage_totals
from tests.conftest import run_async

_GATEWAY_HEADERS = {
    "X-Gateway-Cost-USD": "0.00010000",
    "X-Gateway-Model": "molodetz",
    "X-Gateway-Prompt-Tokens": "500",
    "X-Gateway-Completion-Tokens": "100",
    "X-Gateway-Total-Tokens": "600",
    "X-Gateway-Upstream-Latency-Ms": "400",
    "X-Gateway-Total-Latency-Ms": "450",
}


class _FakeGatewayResp:
    def __init__(self, content, headers):
        self._content = content
        self.headers = headers

    def raise_for_status(self):
        return None

    def json(self):
        return {"choices": [{"message": {"content": self._content}}]}


class _FakeGatewayClient:
    def __init__(self, content, headers):
        self._content = content
        self._headers = headers

    async def __aenter__(self):
        return self

    async def __aexit__(self, *exc):
        return False

    async def post(self, *args, **kwargs):
        return _FakeGatewayResp(self._content, self._headers)


def _fake_gateway(content):
    def factory(*args, **kwargs):
        return _FakeGatewayClient(content, _GATEWAY_HEADERS)

    return factory
_counter_issues_gitea = [0]
@pytest.fixture(autouse=True)
def _init_db_issues_gitea():
    init_db()
    yield
@pytest.fixture
def gitea_env():
    fake = FakeGiteaClient()
    runtime.set_client(fake)
    set_setting("gitea_base_url", "https://gitea.test")
    set_setting("gitea_owner", "retoor")
    set_setting("gitea_repo", "pydevplace")
    set_setting("gitea_token", "test-token")
    set_setting("issue_ai_enhance", "0")
    yield fake
    runtime.set_client(None)
    for table in ("issue_tickets", "issue_comment_authors"):
        for row in list(get_table(table).find()):
            get_table(table).delete(uid=row["uid"])
    for row in list(get_table("jobs").find(kind="issue_create")):
        get_table("jobs").delete(uid=row["uid"])
def _make_user_issues_gitea():
    _counter_issues_gitea[0] += 1
    uid = f"issuetest-user-{_counter_issues_gitea[0]}"
    username = f"issuetester{_counter_issues_gitea[0]}"
    get_table("users").upsert(
        {
            "uid": uid,
            "username": username,
            "terms_version": "1",
            "role": "member",
            "xp": 0,
            "level": 1,
            "is_active": True,
        },
        ["uid"],
    )
    return uid, username
def _unread(user_uid):
    return [
        n
        for n in get_table("notifications").find(user_uid=user_uid)
        if n.get("type") == "issue"
    ]
def _drive_jobs():
    async def run():
        svc = IssueCreateService()
        for _ in range(200):
            await svc.run_once()
            refresh_snapshot()
            pending = [
                r
                for r in get_table("jobs").find(kind="issue_create")
                if r["status"] in ("pending", "running")
            ]
            if not pending and not svc._inflight:
                return
            await asyncio.sleep(0.02)

    run_async(run())
def _role_user(role: str):
    uid, username = _make_user_issues_gitea()
    get_table("users").update({"uid": uid, "role": role}, ["uid"])
    return uid, username
def _open_ticket(fake, role: str):
    import devplacepy.main as m
    from starlette.testclient import TestClient
    from devplacepy.utils import create_session

    uid, _ = _role_user(role)
    issue = run_async(fake.create_issue("Submit does nothing", "details"))
    number = int(issue["number"])
    store.record_ticket(
        number=number,
        author_uid=uid,
        original_title="Submit does nothing",
        original_description="details",
        enhanced_title="Submit does nothing",
        html_url=issue.get("html_url", ""),
        status="open",
    )
    client = TestClient(m.app)
    client.cookies.set("session", create_session(uid))
    return number, client


def test_fake_client_create_and_list(gitea_env):
    async def go():
        issue = await gitea_env.create_issue("Crash on save", "details")
        assert issue["number"] == 1
        assert issue["state"] == "open"
        issues, total = await gitea_env.list_issues(state="open")
        assert total == 1
        assert issues[0]["number"] == 1

    run_async(go())


def test_fake_client_state_and_comments(gitea_env):
    async def go():
        issue = await gitea_env.create_issue("t", "b")
        number = issue["number"]
        await gitea_env.create_comment(number, "first")
        fetched = await gitea_env.get_issue(number)
        assert fetched["comments"] == 1
        closed = await gitea_env.set_state(number, "closed")
        assert closed["state"] == "closed"
        open_issues, _ = await gitea_env.list_issues(state="open")
        assert open_issues == []
        all_issues, total = await gitea_env.list_issues(state="all")
        assert total == 1

    run_async(go())


def test_store_records_and_maps(gitea_env):
    uid, _ = _make_user_issues_gitea()
    store.record_ticket(7, uid, "orig", "desc", "Better", "https://x/7", "open")
    assert store.author_uid_for_issue(7) == uid
    assert store.author_map([7, 8]) == {7: uid}
    cid = store.record_comment_author(99, 7, uid)
    assert cid
    assert store.comment_author_map([99, 100]) == {99: uid}
    assert store.local_comment_ids(7) == {99}


def test_enhance_fallback_when_disabled(gitea_env):
    result = run_async(enhance_ticket("Title", "Body text", gitea_config()))
    assert result.enhanced is False
    assert "## Summary" in result.body
    assert "## Steps to Reproduce" in result.body
    assert result.title == "Title"


def test_create_job_files_issue(gitea_env):
    uid, username = _make_user_issues_gitea()
    job_uid = queue.enqueue(
        "issue_create",
        {"author_uid": uid, "title": "Login broken", "description": "cannot log in"},
        "user",
        uid,
        "Login broken",
    )
    _drive_jobs()
    job = queue.get_job(job_uid)
    assert job["status"] == "done"
    number = job["result"]["number"]
    assert number == 1
    ticket = store.get_ticket(number)
    assert ticket["author_uid"] == uid
    assert _unread(uid)
    issue = run_async(gitea_env.get_issue(number))
    assert username in issue["body"]


def test_create_job_fails_without_config(gitea_env):
    set_setting("gitea_token", "")
    uid, _ = _make_user_issues_gitea()
    job_uid = queue.enqueue(
        "issue_create",
        {"author_uid": uid, "title": "x", "description": "y"},
        "user",
        uid,
        "x",
    )
    _drive_jobs()
    job = queue.get_job(job_uid)
    assert job["status"] == "failed"
    assert "not configured" in job["error"].lower()


def test_poller_notifies_on_developer_reply(gitea_env):
    uid, _ = _make_user_issues_gitea()
    issue = run_async(gitea_env.create_issue("issue", "body"))
    number = issue["number"]
    store.record_ticket(number, uid, "issue", "body", "issue", issue["html_url"], "open")
    store.update_ticket_cache(number, "open", 0)

    gitea_env.add_external_comment(number, "developer", "looking into it")
    before = len(_unread(uid))
    run_async(IssueTrackerService().run_once())
    refresh_snapshot()
    assert len(_unread(uid)) == before + 1
    assert int(store.get_ticket(number)["last_comment_count"]) == 1


def test_poller_ignores_local_comment(gitea_env):
    uid, _ = _make_user_issues_gitea()
    issue = run_async(gitea_env.create_issue("issue", "body"))
    number = issue["number"]
    store.record_ticket(number, uid, "issue", "body", "issue", issue["html_url"], "open")
    store.update_ticket_cache(number, "open", 0)

    comment = run_async(gitea_env.create_comment(number, "me again"))
    store.record_comment_author(comment["id"], number, uid)
    before = len(_unread(uid))
    run_async(IssueTrackerService().run_once())
    refresh_snapshot()
    assert len(_unread(uid)) == before


def test_poller_notifies_on_status_change(gitea_env):
    uid, _ = _make_user_issues_gitea()
    issue = run_async(gitea_env.create_issue("issue", "body"))
    number = issue["number"]
    store.record_ticket(number, uid, "issue", "body", "issue", issue["html_url"], "open")
    store.update_ticket_cache(number, "open", 0)

    run_async(gitea_env.set_state(number, "closed"))
    before = len(_unread(uid))
    run_async(IssueTrackerService().run_once())
    refresh_snapshot()
    assert len(_unread(uid)) == before + 1
    assert store.get_ticket(number)["last_status"] == "closed"


def test_planning_empty_when_no_issues(gitea_env):
    markdown, ai_used = run_async(generate_plan([], gitea_config()))
    assert ai_used is False
    assert markdown.startswith("# Open Tickets Implementation Plan")
    assert "no open tickets" in markdown.lower()


def test_planning_fallback_is_phased_with_verbatim_body(gitea_env):
    issues = [
        {
            "number": 41,
            "title": "Broken functionality",
            "labels": [{"name": "bug"}],
            "body": "Something is broken.\nStep one fails.",
        },
        {
            "number": 7,
            "title": "Show costs",
            "labels": [{"name": "feature"}],
            "body": "Display research costs for admins.",
        },
    ]
    totals = new_usage_totals()
    markdown, ai_used = run_async(generate_plan(issues, gitea_config(), totals))
    assert ai_used is False
    assert "## Execution Order" in markdown
    assert "1. #41 - Broken functionality" in markdown
    assert "## Phase 1: bug" in markdown
    assert "## Phase 2: feature" in markdown
    assert "### #41 Broken functionality" in markdown
    assert "> Something is broken." in markdown
    assert "> Step one fails." in markdown
    assert totals["calls"] == 0


def test_planning_meters_usage_via_gateway(gitea_env, monkeypatch):
    set_setting("issue_ai_enhance", "1")
    refresh_snapshot()
    monkeypatch.setattr(
        "devplacepy.stealth.stealth_async_client",
        _fake_gateway("# Open Tickets Implementation Plan\n\nDone."),
    )
    totals = new_usage_totals()
    issues = [{"number": 1, "title": "t", "labels": [], "body": "b"}]
    markdown, ai_used = run_async(generate_plan(issues, gitea_config(), totals))
    assert ai_used is True
    assert markdown.startswith("# Open Tickets Implementation Plan\n\nDone.")
    assert "# Appendix: Source Tickets (verbatim)" in markdown
    assert totals["calls"] == 1
    assert totals["total_tokens"] == 600


def test_planning_ai_output_always_carries_verbatim_appendix(gitea_env, monkeypatch):
    set_setting("issue_ai_enhance", "1")
    refresh_snapshot()
    monkeypatch.setattr(
        "devplacepy.stealth.stealth_async_client",
        _fake_gateway("# Open Tickets Implementation Plan\n\nSummarised plan only."),
    )
    issues = [
        {
            "number": 12,
            "title": "Add export",
            "labels": [{"name": "feature"}],
            "html_url": "https://gitea.test/retoor/pydevplace/issues/12",
            "body": "Full body line A.\nFull body line B.",
        }
    ]
    markdown, ai_used = run_async(generate_plan(issues, gitea_config()))
    assert ai_used is True
    assert "Summarised plan only." in markdown
    assert "# Appendix: Source Tickets (verbatim)" in markdown
    assert "## #12 Add export" in markdown
    assert "https://gitea.test/retoor/pydevplace/issues/12" in markdown
    assert "> Full body line A." in markdown
    assert "> Full body line B." in markdown


def test_verbatim_tickets_reproduces_full_body_and_labels():
    issues = [
        {
            "number": 5,
            "title": "Fix crash",
            "labels": [{"name": "bug"}, {"name": "urgent"}],
            "html_url": "https://gitea.test/x/5",
            "body": "Steps:\n1. open\n2. boom",
        }
    ]
    markdown = verbatim_tickets(issues)
    assert markdown.startswith("# Appendix: Source Tickets (verbatim)")
    assert "## #5 Fix crash" in markdown
    assert "**Labels**: bug, urgent" in markdown
    assert "**Source**: https://gitea.test/x/5" in markdown
    assert "> Steps:" in markdown
    assert "> 1. open" in markdown
    assert "> 2. boom" in markdown


def test_collect_open_issues_pages_through_fake(gitea_env):
    for index in range(3):
        run_async(gitea_env.create_issue(f"Ticket {index}", f"Body {index}"))
    issues = run_async(collect_open_issues(gitea_env))
    assert {issue["number"] for issue in issues} == {1, 2, 3}


def test_planning_service_filters_to_selected_numbers(gitea_env):
    for index in range(3):
        run_async(gitea_env.create_issue(f"Ticket {index}", f"Body {index}"))
    service = PlanningReportService()

    selected = run_async(service._collect([3, 1]))
    assert [issue["number"] for issue in selected] == [3, 1]

    missing = run_async(service._collect([99]))
    assert missing == []

    everything = run_async(service._collect([]))
    assert {issue["number"] for issue in everything} == {1, 2, 3}


def test_planning_service_parses_payload_numbers():
    service = PlanningReportService()
    job = {"payload": {"numbers": ["4", 7, "x", 7, None]}}
    assert service._selected_numbers(job) == [4, 7]
    assert service._selected_numbers({"payload": {}}) == []


def test_enhance_meters_usage_via_gateway(gitea_env, monkeypatch):
    set_setting("issue_ai_enhance", "1")
    refresh_snapshot()
    monkeypatch.setattr(
        "devplacepy.stealth.stealth_async_client",
        _fake_gateway('{"title": "Fix login", "body": "## Summary\\nFix it."}'),
    )
    totals = new_usage_totals()
    result = run_async(enhance_ticket("login", "broken", gitea_config(), totals))
    assert result.enhanced is True
    assert result.title == "Fix login"
    assert totals["calls"] == 1
    assert abs(totals["cost_usd"] - 0.0001) < 1e-9


def test_enhance_fallback_does_not_meter(gitea_env):
    totals = new_usage_totals()
    result = run_async(enhance_ticket("Title", "Body", gitea_config(), totals))
    assert result.enhanced is False
    assert totals["calls"] == 0


def test_create_job_meters_usage(gitea_env, monkeypatch):
    get_table("issue_usage").delete()
    set_setting("issue_ai_enhance", "1")
    refresh_snapshot()
    monkeypatch.setattr(
        "devplacepy.stealth.stealth_async_client",
        _fake_gateway('{"title": "Login broken", "body": "## Summary\\nbroken."}'),
    )
    uid, _ = _make_user_issues_gitea()
    job_uid = queue.enqueue(
        "issue_create",
        {"author_uid": uid, "title": "login", "description": "cannot log in"},
        "user",
        uid,
        "login",
    )
    _drive_jobs()
    assert queue.get_job(job_uid)["status"] == "done"
    assert get_issue_usage()["calls"] == 1


def test_issue_tracker_collect_metrics(gitea_env):
    get_table("issue_usage").delete()
    add_issue_usage(
        {
            "calls": 3,
            "prompt_tokens": 900,
            "completion_tokens": 300,
            "total_tokens": 1200,
            "cost_usd": 0.003,
            "upstream_latency_ms": 1200.0,
            "total_latency_ms": 1500.0,
        }
    )
    metrics = IssueTrackerService().collect_metrics()
    by_label = {card["label"]: card["value"] for card in metrics["stats"]}
    assert by_label["AI calls"] == 3
    assert by_label["Total tokens"] == 1200
    assert by_label["Total cost"] == "$0.0030"
