import httpx
import pytest

from src import main
from src.following_store import FollowingStore


@pytest.mark.anyio
async def test_confirmed_watch_updates_account_following_collection(monkeypatch, tmp_path):
    store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"a" * 32)
    detail = {
        "repository": "stackchain/api",
        "kind": "issue",
        "number": 42,
        "title": "Make mobile review useful",
        "state": "open",
        "updated_at": "2026-08-23T03:00:00Z",
        "url": "https://forge.example/stackchain/api/issues/42",
        "claimable": True,
    }

    async def preview(repository, kind, number):
        assert (repository, kind, number) == ("stackchain/api", "issue", 42)
        return detail

    async def set_subscription(repository, number, watching):
        return {"watching": watching}

    async def user():
        return {"login": "Timmy"}

    monkeypatch.setattr(main, "_following_store", lambda: store, raising=False)
    monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
    monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
    monkeypatch.setattr(main, "current_user", user)

    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        watched = await client.put(
            "/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
        )
        following = await client.get("/api/v1/following")

    assert watched.status_code == 200
    assert watched.json() == {
        "watching": True,
        "following_synced": True,
        "following_revision": 1,
        "following_count": 1,
    }
    assert following.status_code == 200
    assert following.headers["cache-control"] == "no-store"
    assert following.json() == {"revision": 1, "degraded": False, "refresh_failures": 0, "items": [{
        "repository": "stackchain/api",
        "kind": "issue",
        "number": 42,
        "title": "Make mobile review useful",
        "state": "open",
        "updated_at": "2026-08-23T03:00:00Z",
        "url": "https://forge.example/stackchain/api/issues/42",
        "has_unseen_change": False,
    }]}


@pytest.mark.anyio
async def test_confirmed_pull_watch_round_trips_through_following(monkeypatch, tmp_path):
    store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"p" * 32)
    detail = {
        "repository": "stackchain/api", "kind": "pull", "number": 84,
        "title": "Ship typed Following", "state": "open",
        "updated_at": "2026-08-23T05:00:00Z",
        "url": "https://forge.example/stackchain/api/pulls/84",
    }
    previews = []

    async def preview(repository, kind, number):
        previews.append((repository, kind, number))
        return detail

    async def set_subscription(repository, number, watching):
        return {"watching": watching}

    async def user():
        return {"login": "timmy"}

    monkeypatch.setattr(main, "_following_store", lambda: store)
    monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
    monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
    monkeypatch.setattr(main, "current_user", user)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        watched = await client.put(
            "/api/v1/repos/stackchain/api/issues/84/preview/subscription?kind=pull"
        )
        following = await client.get("/api/v1/following")

    assert watched.status_code == 200
    assert following.status_code == 200
    assert following.json()["items"][0]["kind"] == "pull"
    assert previews == [
        ("stackchain/api", "pull", 84),
        ("stackchain/api", "pull", 84),
    ]


@pytest.mark.anyio
async def test_full_following_collection_rejects_watch_before_gitea_mutation(monkeypatch, tmp_path):
    store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"b" * 32, limit=1)
    store.set_watching("timmy", {
        "repository": "stackchain/api", "number": 7, "title": "Already followed",
        "state": "open", "updated_at": "2026-08-23T02:00:00Z",
        "url": "https://forge.example/stackchain/api/issues/7",
    }, True)
    calls = []

    async def preview(repository, kind, number):
        return {
            "repository": repository, "kind": kind, "number": number,
            "title": "Another issue", "state": "open",
            "updated_at": "2026-08-23T03:00:00Z",
            "url": "https://forge.example/stackchain/api/issues/42", "claimable": True,
        }

    async def set_subscription(*args):
        calls.append(args)
        return {"watching": True}

    async def user():
        return {"login": "timmy"}

    monkeypatch.setattr(main, "_following_store", lambda: store)
    monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
    monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
    monkeypatch.setattr(main, "current_user", user)

    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.put(
            "/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
        )

    assert response.status_code == 503
    assert calls == []
    assert store.get("timmy")["items"][0]["number"] == 7


@pytest.mark.anyio
async def test_following_write_failure_compensates_confirmed_gitea_watch(monkeypatch, tmp_path):
    store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"c" * 32)
    calls = []

    async def preview(repository, kind, number):
        return {
            "repository": repository, "kind": kind, "number": number,
            "title": "Recoverable watch", "state": "open",
            "updated_at": "2026-08-23T03:00:00Z",
            "url": "https://forge.example/stackchain/api/issues/42", "claimable": True,
        }

    async def set_subscription(repository, number, watching):
        calls.append(watching)
        return {"watching": watching}

    async def user():
        return {"login": "timmy"}

    def fail_write(*args):
        raise OSError("disk unavailable")

    monkeypatch.setattr(main, "_following_store", lambda: store)
    monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
    monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
    monkeypatch.setattr(main, "current_user", user)
    monkeypatch.setattr(store, "set_watching", fail_write)

    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.put(
            "/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
        )

    assert response.status_code == 503
    assert response.json() == {"error": "Watch status was not changed. Please retry."}
    assert calls == [True, False]


@pytest.mark.anyio
async def test_uncompensated_following_write_reports_authoritative_partial_outcome(monkeypatch, tmp_path):
    store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"d" * 32)
    calls = []

    async def preview(repository, kind, number):
        return {
            "repository": repository, "kind": kind, "number": number,
            "title": "Split watch", "state": "open",
            "updated_at": "2026-08-23T03:00:00Z",
            "url": "https://forge.example/stackchain/api/issues/42", "claimable": True,
        }

    async def set_subscription(repository, number, watching):
        calls.append(watching)
        return {"watching": True}

    async def user():
        return {"login": "timmy"}

    monkeypatch.setattr(main, "_following_store", lambda: store)
    monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
    monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
    monkeypatch.setattr(main, "current_user", user)
    monkeypatch.setattr(store, "set_watching", lambda *args: (_ for _ in ()).throw(OSError()))

    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.put(
            "/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
        )

    assert response.status_code == 200
    assert response.json() == {
        "watching": True,
        "following_synced": False,
        "error": "Watching in Gitea, but Following could not sync. Retry this action.",
    }
    assert calls == [True, False]


@pytest.mark.anyio
async def test_following_refreshes_changed_items_and_preserves_failed_items(monkeypatch, tmp_path):
    store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"e" * 32)
    first = {
        "repository": "stackchain/api", "number": 42, "title": "Old title",
        "state": "open", "updated_at": "2026-08-23T03:00:00Z",
        "url": "https://forge.example/stackchain/api/issues/42",
    }
    failed = {**first, "number": 43, "title": "Last known",
              "url": "https://forge.example/stackchain/api/issues/43"}
    store.set_watching("timmy", failed, True)
    store.set_watching("timmy", first, True)

    async def preview(repository, kind, number):
        assert (repository, kind) == ("stackchain/api", "issue")
        if number == 43:
            return {**failed, "kind": "issue", "title": ""}
        return {**first, "kind": "issue", "title": "Fresh title", "state": "closed",
                "updated_at": "2026-08-23T04:00:00Z"}

    async def user():
        return {"login": "timmy"}

    monkeypatch.setattr(main, "_following_store", lambda: store)
    monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
    monkeypatch.setattr(main, "current_user", user)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.get("/api/v1/following")

    assert response.status_code == 200
    payload = response.json()
    assert payload["degraded"] is True
    assert payload["refresh_failures"] == 1
    assert [(item["number"], item["has_unseen_change"]) for item in payload["items"]] == [
        (42, True), (43, False)
    ]
    assert payload["items"][0]["title"] == "Fresh title"
    assert payload["items"][1]["title"] == "Last known"


@pytest.mark.anyio
async def test_following_acknowledges_only_the_exact_loaded_revision(monkeypatch, tmp_path):
    store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"f" * 32)
    item = {
        "repository": "stackchain/api", "kind": "pull", "number": 42, "title": "Changed",
        "state": "open", "updated_at": "2026-08-23T03:00:00Z",
        "url": "https://forge.example/stackchain/api/issues/42",
    }
    store.set_watching("timmy", item, True)
    store.refresh("timmy", [{**item, "updated_at": "2026-08-23T04:00:00Z"}])

    async def user():
        return {"login": "timmy"}

    monkeypatch.setattr(main, "_following_store", lambda: store)
    monkeypatch.setattr(main, "current_user", user)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.put(
            "/api/v1/following/stackchain/api/issues/42/seen?kind=pull",
            json={"updated_at": "2026-08-23T04:00:00Z"},
        )

    assert response.status_code == 200
    assert response.json()["items"][0]["has_unseen_change"] is False


@pytest.mark.anyio
async def test_following_keep_restores_only_the_loaded_revision(monkeypatch, tmp_path):
    store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"f" * 32)
    item = {
        "repository": "stackchain/api", "kind": "pull", "number": 42, "title": "Changed",
        "state": "open", "updated_at": "2026-08-23T03:00:00Z",
        "url": "https://forge.example/stackchain/api/pulls/42",
    }
    store.set_watching("timmy", item, True)
    changed = {**item, "updated_at": "2026-08-23T04:00:00Z"}
    store.refresh("timmy", [changed])
    store.acknowledge("timmy", item["repository"], item["number"], changed["updated_at"], kind="pull")

    async def user():
        return {"login": "timmy"}

    monkeypatch.setattr(main, "_following_store", lambda: store)
    monkeypatch.setattr(main, "current_user", user)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.put(
            "/api/v1/following/stackchain/api/issues/42/keep?kind=pull",
            json={"updated_at": changed["updated_at"]},
        )

    assert response.status_code == 200
    assert response.json()["items"][0]["has_unseen_change"] is True


@pytest.mark.anyio
async def test_closed_following_issue_can_be_unwatched_but_not_newly_watched(monkeypatch, tmp_path):
    store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=b"g" * 32)
    closed = {
        "repository": "stackchain/api", "number": 42, "title": "Finished work",
        "state": "closed", "updated_at": "2026-08-23T04:00:00Z",
        "url": "https://forge.example/stackchain/api/issues/42",
    }
    store.set_watching("timmy", closed, True)
    mutations = []

    async def preview(repository, kind, number):
        return {**closed, "kind": "issue"}

    async def set_subscription(repository, number, watching):
        mutations.append((repository, number, watching))
        return {"watching": watching}

    async def user():
        return {"login": "timmy"}

    monkeypatch.setattr(main, "_following_store", lambda: store)
    monkeypatch.setattr(main.gitea_proxy, "work_preview", preview)
    monkeypatch.setattr(main.gitea_proxy, "set_issue_subscription", set_subscription)
    monkeypatch.setattr(main, "current_user", user)
    transport = httpx.ASGITransport(app=main.app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        watch = await client.put(
            "/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
        )
        unwatch = await client.delete(
            "/api/v1/repos/stackchain/api/issues/42/preview/subscription?kind=issue"
        )

    assert watch.status_code == 404
    assert unwatch.status_code == 200
    assert unwatch.json() == {
        "watching": False,
        "following_synced": True,
        "following_revision": 2,
        "following_count": 0,
    }
    assert mutations == [("stackchain/api", 42, False)]
    assert store.get("timmy")["items"] == []
