from __future__ import annotations

from datetime import UTC, datetime

import pytest

from api.services import recently_on_scope as ros
from api.services.markitect_templates import MarkitectUnavailable


async def _create_domain(client, slug="digest", name="Digest Domain"):
    response = await client.post("/domains/", json={"slug": slug, "name": name})
    assert response.status_code == 201, response.text
    return response.json()


async def _create_topic(client, domain_slug="digest", slug="digest-topic", title="Digest Topic"):
    response = await client.post("/topics/", json={"slug": slug, "title": title, "domain": domain_slug})
    assert response.status_code == 201, response.text
    return response.json()


from tests.conftest import create_test_repo, create_test_workplan


async def _create_workstream(client, topic_id, slug="digest-ws", title="Digest Workstream", domain_slug="digest"):
    repo = await create_test_repo(client, domain_slug=domain_slug, slug=f"{slug}-repo")
    return await create_test_workplan(
        client, repo_id=repo["id"], topic_id=topic_id, slug=slug, title=title,
    )


async def _create_task(client, workplan_id, title="Digest task"):
    response = await client.post("/tasks/", json={"workplan_id": workplan_id, "title": title})
    assert response.status_code == 201, response.text
    return response.json()


@pytest.fixture
def fake_markitect(monkeypatch):
    def _inspect(_template_path):
        return {"valid": True, "variables": []}

    def _render(_template_path, data):
        counts = data["source_counts"]
        return f"""---
type: recently_on_scope_digest
domain_slug: "{data["domain"]["slug"]}"
domain_name: "{data["domain"]["name"]}"
range: "{data["window"]["range"]}"
since: "{data["window"]["since"]}"
until: "{data["window"]["until"]}"
generated_at: "{data["generated_at"]}"
template_version: "{data["template_version"]}"
source_counts:
  progress_events: {counts["progress_events"]}
  decisions: {counts["decisions"]}
  workstreams: {counts["workstreams"]}
  tasks: {counts["tasks"]}
  repos: {counts["repos"]}
  attention_items: {counts["attention_items"]}
---

# RecentlyOnScope - {data["domain"]["name"]}

{data["progress_section"]}
"""

    monkeypatch.setattr(ros, "inspect_markdown_template", _inspect)
    monkeypatch.setattr(ros, "render_markdown_template", _render)


def test_resolve_window_defaults_to_one_hour():
    now = datetime(2026, 5, 22, 12, 0, tzinfo=UTC)
    window = ros.resolve_window(now=now)

    assert window.range == "1h"
    assert window.since == datetime(2026, 5, 22, 11, 0, tzinfo=UTC)
    assert window.until == now
    assert ros.report_id_for(window) == "20260522T120000Z--1h"


def test_resolve_window_exact_range_id():
    since = datetime(2026, 5, 22, 10, 30, tzinfo=UTC)
    until = datetime(2026, 5, 22, 12, 0, tzinfo=UTC)
    window = ros.resolve_window("6h", since=since, until=until)

    assert window.exact is True
    assert ros.report_id_for(window) == "20260522T103000Z--20260522T120000Z"


def test_resolve_window_rejects_ambiguous_duration():
    with pytest.raises(ValueError):
        ros.resolve_window("hour")


class TestRecentlyOnScopeRoutes:
    async def test_generate_list_and_read_digest(self, client, tmp_path, monkeypatch, fake_markitect):
        monkeypatch.setattr(ros.settings, "state_hub_report_dir", str(tmp_path))

        await _create_domain(client)
        topic = await _create_topic(client)
        workstream = await _create_workstream(client, topic["id"])
        task = await _create_task(client, workstream["id"], title="Build digest source")
        await client.patch(
            f"/tasks/{task['id']}",
            json={"needs_human": True, "intervention_note": "Review the generated summary."},
        )
        progress = await client.post(
            "/progress/",
            json={
                "topic_id": topic["id"],
                "workstream_id": workstream["id"],
                "task_id": task["id"],
                "event_type": "note",
                "summary": "Built digest source",
                "author": "codex",
            },
        )
        assert progress.status_code == 201, progress.text

        await _create_domain(client, slug="elsewhere", name="Elsewhere")
        other_topic = await _create_topic(client, domain_slug="elsewhere", slug="other-topic")
        other_progress = await client.post(
            "/progress/",
            json={"topic_id": other_topic["id"], "event_type": "note", "summary": "Do not include"},
        )
        assert other_progress.status_code == 201, other_progress.text

        response = await client.post("/domains/digest/recently-on-scope/", json={"range": "1d"})
        assert response.status_code == 201, response.text
        body = response.json()

        assert body["domain_slug"] == "digest"
        assert body["range"] == "1d"
        assert body["source_counts"]["progress_events"] == 1
        assert body["source_counts"]["attention_items"] == 1
        assert "Built digest source" in body["markdown"]

        listed = await client.get("/domains/digest/recently-on-scope/")
        assert listed.status_code == 200
        reports = listed.json()
        assert len(reports) == 1
        assert reports[0]["id"] == body["id"]

        markdown = await client.get(f"/domains/digest/recently-on-scope/{body['id']}")
        assert markdown.status_code == 200
        assert markdown.headers["content-type"].startswith("text/markdown")
        assert "RecentlyOnScope - Digest Domain" in markdown.text

    async def test_generate_rejects_bad_range(self, client, tmp_path, monkeypatch, fake_markitect):
        monkeypatch.setattr(ros.settings, "state_hub_report_dir", str(tmp_path))
        await _create_domain(client)

        response = await client.post("/domains/digest/recently-on-scope/", json={"range": "yesterday"})

        assert response.status_code == 422

    async def test_generate_reports_missing_markitect(self, client, tmp_path, monkeypatch):
        monkeypatch.setattr(ros.settings, "state_hub_report_dir", str(tmp_path))

        def _missing_markitect(_template_path):
            raise MarkitectUnavailable("MarkiTect unavailable")

        monkeypatch.setattr(ros, "inspect_markdown_template", _missing_markitect)
        await _create_domain(client)

        response = await client.post("/domains/digest/recently-on-scope/", json={"range": "1h"})

        assert response.status_code == 503

    async def test_missing_report_returns_404(self, client, tmp_path, monkeypatch):
        monkeypatch.setattr(ros.settings, "state_hub_report_dir", str(tmp_path))

        response = await client.get("/domains/digest/recently-on-scope/20260522T120000Z--1h")

        assert response.status_code == 404

    async def test_hourly_batch_generates_only_domains_with_activity(
        self,
        client,
        tmp_path,
        monkeypatch,
        fake_markitect,
    ):
        monkeypatch.setattr(ros.settings, "state_hub_report_dir", str(tmp_path))

        await _create_domain(client)
        topic = await _create_topic(client)
        workstream = await _create_workstream(client, topic["id"])
        task = await _create_task(client, workstream["id"], title="Batch source")
        progress = await client.post(
            "/progress/",
            json={
                "topic_id": topic["id"],
                "workstream_id": workstream["id"],
                "task_id": task["id"],
                "event_type": "note",
                "summary": "Batch source changed",
            },
        )
        assert progress.status_code == 201, progress.text
        await _create_domain(client, slug="quiet", name="Quiet Domain")

        response = await client.post("/recently-on-scope/hourly", json={"range": "1d"})

        assert response.status_code == 201, response.text
        body = response.json()
        assert [item["domain_slug"] for item in body["generated"]] == ["digest"]
        assert [item["domain_slug"] for item in body["skipped"]] == ["quiet"]
        assert body["failed"] == []
        assert body["generated"][0]["source_counts"]["progress_events"] == 1
        assert body["progress_event_id"] is not None

        events = await client.get("/progress/", params={"event_type": "recently_on_scope_hourly"})
        assert events.status_code == 200, events.text
        assert events.json()[0]["detail"]["generated"][0]["domain_slug"] == "digest"

    async def test_hourly_batch_reports_no_active_domains(
        self,
        client,
        tmp_path,
        monkeypatch,
        fake_markitect,
    ):
        monkeypatch.setattr(ros.settings, "state_hub_report_dir", str(tmp_path))
        await _create_domain(client, slug="quiet", name="Quiet Domain")

        response = await client.post("/recently-on-scope/hourly", json={"range": "1d"})

        assert response.status_code == 201, response.text
        body = response.json()
        assert body["generated"] == []
        assert [item["domain_slug"] for item in body["skipped"]] == ["quiet"]
        assert body["failed"] == []

    async def test_hourly_batch_is_idempotent_for_exact_window(
        self,
        client,
        tmp_path,
        monkeypatch,
        fake_markitect,
    ):
        monkeypatch.setattr(ros.settings, "state_hub_report_dir", str(tmp_path))
        await _create_domain(client)
        topic = await _create_topic(client)
        workstream = await _create_workstream(client, topic["id"])
        await _create_task(client, workstream["id"], title="Idempotent source")
        payload = {
            "range": "1h",
            "since": "2000-01-01T00:00:00Z",
            "until": "2100-01-01T00:00:00Z",
        }

        first = await client.post("/recently-on-scope/hourly", json=payload)
        second = await client.post("/recently-on-scope/hourly", json=payload)

        assert first.status_code == 201, first.text
        assert second.status_code == 201, second.text
        assert first.json()["generated"][0]["id"] == "20000101T000000Z--21000101T000000Z"
        listed = await client.get("/domains/digest/recently-on-scope/")
        assert listed.status_code == 200
        assert [item["id"] for item in listed.json()] == ["20000101T000000Z--21000101T000000Z"]

    async def test_hourly_batch_continues_after_domain_failure(
        self,
        client,
        tmp_path,
        monkeypatch,
        fake_markitect,
    ):
        monkeypatch.setattr(ros.settings, "state_hub_report_dir", str(tmp_path))
        original_render = ros._render_report_from_data

        def flaky_render(data, window):
            if data["domain"]["slug"] == "broken":
                raise RuntimeError("render failed")
            return original_render(data, window)

        monkeypatch.setattr(ros, "_render_report_from_data", flaky_render)

        await _create_domain(client, slug="broken", name="Broken Domain")
        broken_topic = await _create_topic(client, domain_slug="broken", slug="broken-topic")
        broken_workstream = await _create_workstream(
            client, broken_topic["id"], slug="broken-ws", domain_slug="broken",
        )
        await _create_task(client, broken_workstream["id"], title="Broken source")

        await _create_domain(client, slug="good", name="Good Domain")
        good_topic = await _create_topic(client, domain_slug="good", slug="good-topic")
        good_workstream = await _create_workstream(
            client, good_topic["id"], slug="good-ws", domain_slug="good",
        )
        await _create_task(client, good_workstream["id"], title="Good source")

        response = await client.post("/recently-on-scope/hourly", json={"range": "1d"})

        assert response.status_code == 201, response.text
        body = response.json()
        assert [item["domain_slug"] for item in body["generated"]] == ["good"]
        assert [item["domain_slug"] for item in body["failed"]] == ["broken"]
        assert body["failed"][0]["error"] == "render failed"
