from __future__ import annotations

import json
from collections.abc import Generator
from pathlib import Path
from unittest.mock import patch

import pygit2
import pytest
from fastapi.testclient import TestClient
from pygit2 import Signature

from pygitweb.config import settings
from pygitweb.main import app

SIG = Signature("tester", "tester@example.com")


def _commit_tree(repo: pygit2.Repository, repo_dir: Path, files: dict[str, str], message: str) -> None:
    for rel, content in files.items():
        path = repo_dir / rel
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(content, encoding="utf-8")
    index = repo.index
    for rel in files:
        index.add(rel)
    index.write()
    tree = index.write_tree()
    parents = [repo.head.target] if not repo.head_is_unborn else []
    repo.create_commit("HEAD", SIG, SIG, message, tree, parents)


@pytest.fixture
def blog_env(tmp_path: Path) -> Generator[dict[str, str | TestClient], None, None]:
    root = tmp_path / "blog-root"
    repo_dir = root / "demo"
    repo_dir.mkdir(parents=True)
    repo = pygit2.init_repository(str(repo_dir), bare=False)
    manifest = [
        {
            "name": "Hello Blog",
            "url": "?article=articles/2024.01.01/post.md",
            "tags": "intro test",
            "created": "Jan 1 2024",
            "preview": "",
            "prev": "",
            "next": "",
        }
    ]
    _commit_tree(
        repo,
        repo_dir,
        {
            "README.md": "seed\n",
            "articles/manifest.json": json.dumps(manifest),
            "articles/2024.01.01/post.md": "# Hello\n\nBody text.\n",
            "assets/note.txt": "asset\n",
        },
        "add blog",
    )
    with (
        patch.object(settings, "PROJECTROOT", str(root)),
        patch.object(settings, "PROJECTS_LIST", str(root)),
        patch.object(settings, "STRICT_EXPORT", False),
        patch.object(settings, "EXPORT_OK", ""),
        patch.object(settings, "AUTH", False),
    ):
        yield {"project": "demo", "client": TestClient(app), "root": str(root)}


@pytest.fixture
def no_blog_env(tmp_path: Path) -> Generator[dict[str, str | TestClient], None, None]:
    root = tmp_path / "no-blog-root"
    repo_dir = root / "plain"
    repo_dir.mkdir(parents=True)
    repo = pygit2.init_repository(str(repo_dir), bare=False)
    _commit_tree(repo, repo_dir, {"README.md": "no blog here\n"}, "initial")
    with (
        patch.object(settings, "PROJECTROOT", str(root)),
        patch.object(settings, "PROJECTS_LIST", str(root)),
        patch.object(settings, "STRICT_EXPORT", False),
        patch.object(settings, "EXPORT_OK", ""),
        patch.object(settings, "AUTH", False),
    ):
        yield {"project": "plain", "client": TestClient(app)}


def test_blog_page_renders_when_manifest_exists(blog_env: dict[str, str | TestClient]) -> None:
    client = blog_env["client"]
    assert isinstance(client, TestClient)
    resp = client.get(f"/project/{blog_env['project']}/blog/")
    assert resp.status_code == 200
    assert 'id="blog-root"' in resp.text
    assert 'data-manifest="articles/manifest.json"' in resp.text
    assert f'data-content-base="/project/{blog_env["project"]}/blog/raw/"' in resp.text
    assert "/static/blog.js" in resp.text
    assert "navbar-vertical" not in resp.text
    header, _, body = resp.text.partition('<div class="page-wrapper">')
    assert 'class="navbar navbar-expand-md blog-topbar' in header
    assert "blog-search" in header
    assert "blog-search" not in body
    assert 'id="navbar-user"' in resp.text
    assert 'href="/"' in resp.text
    assert f"/login?next=/project/{blog_env['project']}/blog/" in resp.text


def test_blog_page_404_without_manifest(no_blog_env: dict[str, str | TestClient]) -> None:
    client = no_blog_env["client"]
    assert isinstance(client, TestClient)
    resp = client.get(f"/project/{no_blog_env['project']}/blog/")
    assert resp.status_code == 404


def test_blog_raw_serves_manifest_and_markdown(blog_env: dict[str, str | TestClient]) -> None:
    client = blog_env["client"]
    assert isinstance(client, TestClient)
    project = blog_env["project"]
    manifest_resp = client.get(f"/project/{project}/blog/raw/articles/manifest.json")
    assert manifest_resp.status_code == 200
    payload = manifest_resp.json()
    assert payload[0]["name"] == "Hello Blog"

    md_resp = client.get(f"/project/{project}/blog/raw/articles/2024.01.01/post.md")
    assert md_resp.status_code == 200
    assert "# Hello" in md_resp.text

    asset_resp = client.get(f"/project/{project}/blog/raw/assets/note.txt")
    assert asset_resp.status_code == 200
    assert asset_resp.text == "asset\n"


def test_blog_raw_rejects_missing_and_traversal(blog_env: dict[str, str | TestClient]) -> None:
    client = blog_env["client"]
    assert isinstance(client, TestClient)
    project = blog_env["project"]
    assert client.get(f"/project/{project}/blog/raw/missing.md").status_code == 404
    assert client.get(f"/project/{project}/blog/raw/../README.md").status_code == 404


def test_summary_includes_blog_link(blog_env: dict[str, str | TestClient]) -> None:
    client = blog_env["client"]
    assert isinstance(client, TestClient)
    resp = client.get(f"/project/{blog_env['project']}")
    assert resp.status_code == 200
    assert ">Blog</td>" in resp.text or ">Blog<" in resp.text
    assert f'href="/project/{blog_env["project"]}/blog/">view blog</a>' in resp.text


def test_summary_shows_no_blog_when_missing(no_blog_env: dict[str, str | TestClient]) -> None:
    client = no_blog_env["client"]
    assert isinstance(client, TestClient)
    resp = client.get(f"/project/{no_blog_env['project']}")
    assert resp.status_code == 200
    assert ">no blog</a>" in resp.text
    assert 'style="color: #999; cursor: not-allowed;"' in resp.text