"""Release-channel checks and updates against actual repositories and HTTP feeds."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from threading import Thread
from types import SimpleNamespace
import subprocess
import urllib.request
from urllib.parse import urlsplit

import pytest

from hermes_cli import main, update_cmd
from hermes_cli.source_releases import resolve_source_release
from hermes_cli.update_channel import set_install_channel


def git(root, *args):
    return subprocess.run(
        ["git", *args], cwd=root, check=True, capture_output=True, text=True,
    ).stdout.strip()


@pytest.fixture(params=["utf-8", "utf-8-sig"])
def releases(tmp_path, monkeypatch, request):
    origin = tmp_path / "origin"
    origin.mkdir()
    git(origin, "init", "-b", "main")
    git(origin, "config", "user.name", "Release Fixture")
    git(origin, "config", "user.email", "fixture@example.invalid")
    git(origin, "config", "commit.gpgsign", "false")
    git(origin, "config", "tag.gpgsign", "false")
    commits = []
    for label in ("old", "stable", "canary", "unpublished"):
        (origin / "content.txt").write_text(label, encoding="utf-8")
        git(origin, "add", ".")
        git(origin, "commit", "-m", label)
        commits.append(git(origin, "rev-parse", "HEAD"))
    tags = {"stable": "v1.2.3", "canary": "v1.2.3+canary.20260911T125822Z"}
    git(origin, "tag", "-a", tags["stable"], commits[1], "-m", "stable")
    git(origin, "tag", "-a", tags["canary"], commits[2], "-m", "canary")
    git(origin, "tag", "v99.0.0", commits[3])
    git(origin, "tag", "v99.0.1+canary.20260912T125822Z", commits[3])
    checkout = tmp_path / "checkout"
    git(tmp_path, "clone", str(origin), str(checkout))
    git(checkout, "config", "user.name", "Release Fixture")
    git(checkout, "config", "user.email", "fixture@example.invalid")
    git(checkout, "config", "commit.gpgsign", "false")
    git(checkout, "checkout", "--detach", commits[0])
    monkeypatch.setattr(main, "PROJECT_ROOT", checkout)
    monkeypatch.setenv("HERMES_INSTALL_ROOT", str(checkout))
    monkeypatch.delenv("HERMES_MANAGED", raising=False)

    responses = {}
    requests = []
    for channel, tag in tags.items():
        responses[f"/releases/{channel}/index.html"] = (
            f'<meta name="hermes-build" content="{tag}">'
        )
        responses[f"/repos/NousResearch/hermes-agent/releases/tags/{tag}"] = {
            "tag_name": tag, "draft": False, "prerelease": channel == "canary",
        }
        responses[f"/repos/NousResearch/hermes-agent/commits/{tag}"] = {
            "sha": commits[1 if channel == "stable" else 2],
        }
    responses["/releases/stable/release-candidates.json"] = {
        "tag": tags["stable"], "commit": commits[1],
    }

    class Handler(BaseHTTPRequestHandler):
        def do_GET(self):
            requests.append(self.path)
            data = responses.get(self.path)
            self.send_response(404 if data is None else 200)
            self.end_headers()
            if data is not None:
                self.wfile.write((data if isinstance(data, str) else json.dumps(data)).encode(request.param))

        def log_message(self, format, *args):
            pass

    server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
    thread = Thread(target=server.serve_forever, daemon=True)
    thread.start()
    open_url = urllib.request.urlopen

    def local_urlopen(request, *args, **kwargs):
        url = request.full_url if isinstance(request, urllib.request.Request) else request
        parsed = urlsplit(url)
        return open_url(f"http://127.0.0.1:{server.server_port}{parsed.path}"
                        + (f"?{parsed.query}" if parsed.query else ""), *args, **kwargs)

    monkeypatch.setattr(urllib.request, "urlopen", local_urlopen)
    # Legacy pointer tests below retain their HTTP/tag boundary. New CLI callers
    # consume the protocol reader, whose complete schema is tested independently.
    from hermes_cli import source_releases
    def resolve_channel(name, repository):
        record = {"name": name, "repository": repository, "state": "active",
                  "policy": "source-branch" if name == "main" else "preview"}
        manifest = None
        if name == "main":
            record["delivery"] = {"kind": "source-branch", "branch": "main"}
        else:
            manifest = {"request": {"commit": commits[1 if name == "stable" else 2],
                "sourceVersion": tags[name].removeprefix("v"), "buildId": "legacy-fixture"}}
        return SimpleNamespace(requested=record, terminal=record, manifest=manifest)
    monkeypatch.setattr(source_releases, "_resolve_channel", resolve_channel)
    yield SimpleNamespace(root=checkout, origin=origin, commits=commits,
                          tags=tags, responses=responses, requests=requests)
    server.shutdown()
    server.server_close()
    thread.join()


@pytest.mark.parametrize("git_cmd", [["git"], None], ids=["git", "no-git"])
def test_stable_resolution_uses_promoted_pointer_not_highest_tag(releases, git_cmd):
    assert resolve_source_release("stable", git_cmd, releases.root) == (
        releases.tags["stable"], releases.commits[1],
    )
    assert releases.requests


@pytest.mark.parametrize("channel", ["stable", "canary"])
@pytest.mark.parametrize("start", ["old", "ahead", "local"])
def test_source_check_and_apply_land_on_selected_release(releases, monkeypatch, capsys, channel, start):
    if start != "old":
        git(releases.root, "checkout", "-b", "my-work", releases.commits[3])
    if start == "local":
        (releases.root / "my-work.txt").write_text("committed local work\n")
        git(releases.root, "add", ".")
        git(releases.root, "commit", "-m", "local work")
        (releases.root / "notes.txt").write_text("uncommitted notes\n")
    branch_sha = git(releases.root, "rev-parse", "HEAD")
    set_install_channel(channel, releases.root)
    before = git(releases.root, "rev-parse", "HEAD")
    update_cmd._cmd_update_check()
    assert releases.tags[channel] in capsys.readouterr().out
    assert git(releases.root, "rev-parse", "HEAD") == before

    # Exercise the real selection/fetch/checkout path, not dependency installation
    # or live service management. No host OS is simulated.
    opts = update_cmd._UpdateOptions(
        pre_update_version=None, gw_input_fn=None,
        assume_yes=True, keep_stash=False, switch_branch=False, discard_local_changes=False,
    )
    monkeypatch.setattr(update_cmd, "_resolve_update_options", lambda *_: opts)
    monkeypatch.setattr(update_cmd, "_begin_update_receipt_and_plan", lambda *_: None)
    monkeypatch.setattr(main, "_run_pre_update_backup", lambda *_: None)
    monkeypatch.setattr(main, "_pause_windows_gateways_for_update", lambda: None)
    monkeypatch.setattr(update_cmd, "_prepare_git_command", lambda: (False, ["git"], False))
    applied = []
    monkeypatch.setattr(update_cmd, "_complete_source_update", lambda request: applied.append(request))
    args = SimpleNamespace(branch=None, channel=None, force_venv=True)
    update_cmd._cmd_update_impl(args, False)
    expected = releases.commits[1 if channel == "stable" else 2]
    assert len(applied) == 1
    assert applied[0]["expected_sha"] == expected
    assert applied[0]["source"] == str(releases.root.resolve())
    assert git(releases.root, "rev-parse", "HEAD") == expected
    if start != "old":
        assert git(releases.root, "rev-parse", "my-work") == branch_sha
    if start == "local":
        assert (releases.root / "notes.txt").read_text() == "uncommitted notes\n"
    update_cmd._cmd_update_check()
    assert "Up to date with" in capsys.readouterr().out


def test_source_check_honors_transient_channel_without_rewriting_record(releases, capsys):
    set_install_channel("stable", releases.root)
    args = SimpleNamespace(branch=None, channel="canary", check=True)
    main.cmd_update(args)
    assert releases.tags["canary"] in capsys.readouterr().out
    update_cmd._cmd_update_check()
    assert releases.tags["stable"] in capsys.readouterr().out


def test_fork_origin_uses_its_own_published_release_not_the_official_pointer(releases):
    url = "https://github.com/Fixture/hermes-agent.git"
    git(releases.root, "config", "remote.origin.url", url)
    git(releases.root, "config", f"url.{releases.origin}.insteadOf", url)
    tag = releases.tags["stable"]
    git(releases.origin, "tag", "-f", tag, releases.commits[3])
    releases.responses["/repos/Fixture/hermes-agent/releases/latest"] = {
        "tag_name": tag, "draft": False, "prerelease": False,
    }
    releases.responses[f"/repos/Fixture/hermes-agent/commits/{tag}"] = {"sha": releases.commits[3]}
    assert resolve_source_release("stable", ["git"], releases.root) == (tag, releases.commits[3])
    assert not any(path.startswith("/releases/") for path in releases.requests)


def test_zip_fallback_keeps_selected_repository_and_commit(releases, monkeypatch):
    from hermes_cli import update_cmd_zip

    seen = []
    monkeypatch.setattr(update_cmd_zip, "_abort_zip_update_if_dirty_tree", lambda: None)
    class DownloadBoundary(Exception):
        pass
    def download(branch, url):
        seen.append(url)
        raise DownloadBoundary
    monkeypatch.setattr(update_cmd_zip, "_download_and_swap_zip", download)
    with pytest.raises(DownloadBoundary):
        update_cmd_zip._update_via_zip(
            SimpleNamespace(branch=None), target_sha=releases.commits[2],
            target_repository="Fixture/hermes-agent", completion_request={})
    assert seen == [f"https://github.com/Fixture/hermes-agent/archive/{releases.commits[2]}.zip"]


@pytest.mark.parametrize("git_cmd", [["git"], None], ids=["git", "no-git"])
def test_selected_draft_never_falls_back_to_other_tags(releases, git_cmd):
    releases.responses[f"/repos/NousResearch/hermes-agent/releases/tags/{releases.tags['stable']}"]["draft"] = True
    assert resolve_source_release("stable", git_cmd, releases.root) == (None, None)
    assert not any("/tags?" in path for path in releases.requests)


def test_main_check_still_uses_branch_without_release_requests(releases, capsys):
    set_install_channel("main", releases.root)
    update_cmd._cmd_update_check()
    assert "behind origin/main" in capsys.readouterr().out
    assert releases.requests == []


@pytest.mark.parametrize("channel", ["stable", "canary"])
def test_origin_tag_cannot_substitute_a_fork_commit(releases, channel):
    git(releases.origin, "tag", "-f", releases.tags[channel], releases.commits[3])
    assert resolve_source_release(channel, ["git"], releases.root) == (None, None)
    # Without git, the same selection remains pinned to the official commit.
    assert resolve_source_release(channel) == (
        releases.tags[channel], releases.commits[1 if channel == "stable" else 2],
    )


@pytest.mark.parametrize("channel", ["stable", "canary"])
def test_missing_pointers_fall_back_only_to_published_releases(releases, channel):
    releases.responses.pop("/releases/stable/release-candidates.json")
    releases.responses.pop(f"/releases/{channel}/index.html")
    published = releases.responses[f"/repos/NousResearch/hermes-agent/releases/tags/{releases.tags[channel]}"]
    releases.responses["/repos/NousResearch/hermes-agent/releases/latest"] = published
    releases.responses["/repos/NousResearch/hermes-agent/releases?per_page=100&page=1"] = [
        {"tag_name": "v99.0.1+canary.20260912T125822Z", "draft": True, "prerelease": True},
        published,
    ]
    assert resolve_source_release(channel, ["git"], releases.root) == (
        releases.tags[channel], releases.commits[1 if channel == "stable" else 2],
    )
    assert not any("/tags?" in path for path in releases.requests)
    assert f"/repos/NousResearch/hermes-agent/releases/tags/{releases.tags[channel]}" not in releases.requests
