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