"""Tests for scripts/adapters/obsidian.py."""
from pathlib import Path
import subprocess
import pytest

REPO_ROOT = Path(__file__).resolve().parents[3]
ADAPTER = REPO_ROOT / "scripts/adapters/obsidian.py"
VAULT = REPO_ROOT / "scripts/adapters/examples/obsidian/input_fixture/vault"
EXPECTED_PASSPORT = REPO_ROOT / "scripts/adapters/examples/obsidian/expected_passport.yaml"
EXPECTED_REJECTION = REPO_ROOT / "scripts/adapters/examples/obsidian/expected_rejection_log.yaml"


def _run(*args):
    return subprocess.run(
        ["python", str(ADAPTER)] + list(args),
        capture_output=True,
        text=True,
        cwd=REPO_ROOT,
    )


def test_adapter_exists():
    assert ADAPTER.exists()


def test_happy_path(tmp_path, load_yaml, clean_timestamps):
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(VAULT), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0, res.stderr
    assert clean_timestamps(load_yaml(p)) == clean_timestamps(load_yaml(EXPECTED_PASSPORT))
    assert clean_timestamps(load_yaml(r)) == clean_timestamps(load_yaml(EXPECTED_REJECTION))


def test_missing_vault_fails_loud(tmp_path):
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(tmp_path / "nope"), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 1


def test_templates_dir_skipped(tmp_path, load_yaml):
    vault = tmp_path / "v"
    (vault / "_templates").mkdir(parents=True)
    (vault / "_templates/tmpl.md").write_text(
        "---\ncitekey: tmpl2024\ntitle: T\nauthors:\n  - family: X\nyear: 2024\n---\n",
        encoding="utf-8",
    )
    (vault / "note.md").write_text(
        "---\ncitekey: note2024\ntitle: Real\nauthors:\n  - family: Y\nyear: 2024\n---\nbody\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0
    doc = load_yaml(p)
    keys = [e["citation_key"] for e in doc["literature_corpus"]]
    assert "tmpl2024" not in keys
    assert "note2024" in keys


def test_obsidian_dir_skipped(tmp_path, load_yaml):
    vault = tmp_path / "v"
    (vault / ".obsidian").mkdir(parents=True)
    (vault / ".obsidian/ignored.md").write_text(
        "---\ncitekey: skip2024\ntitle: T\nauthors:\n  - family: X\nyear: 2024\n---\n",
        encoding="utf-8",
    )
    (vault / "real.md").write_text(
        "---\ncitekey: real2024\ntitle: R\nauthors:\n  - family: Y\nyear: 2024\n---\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    doc = load_yaml(p)
    keys = [e["citation_key"] for e in doc["literature_corpus"]]
    assert "skip2024" not in keys
    assert "real2024" in keys


def test_convention_a_source_pointer_is_obsidian_uri(tmp_path, load_yaml):
    """source_pointer for Convention A must be an obsidian:// URI (deterministic, not file://)."""
    vault = tmp_path / "myvault"
    vault.mkdir()
    (vault / "paper.md").write_text(
        "---\ncitekey: smith2024\ntitle: A Study\nauthors:\n  - family: Smith\nyear: 2024\n---\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0, res.stderr
    doc = load_yaml(p)
    entry = doc["literature_corpus"][0]
    assert entry["source_pointer"].startswith("obsidian://open?vault=")
    assert "myvault" in entry["source_pointer"]
    assert "paper" in entry["source_pointer"]


def test_convention_b_source_pointer_is_obsidian_uri(tmp_path, load_yaml):
    """source_pointer for Convention B (Karpathy-style) must also be obsidian:// URI."""
    vault = tmp_path / "mybvault"
    vault.mkdir()
    (vault / "jones2022review.md").write_text(
        "---\nsource: https://doi.org/10.999/abc\n---\n\n# Review of methods\n\n**Authors**: Jones, A.\n**Year**: 2022\n\nContent.\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0, res.stderr
    doc = load_yaml(p)
    entry = doc["literature_corpus"][0]
    assert entry["source_pointer"].startswith("obsidian://open?vault=")
    assert "mybvault" in entry["source_pointer"]


def test_deterministic_output(tmp_path, load_yaml, clean_timestamps):
    p1 = tmp_path / "p1.yaml"
    r1 = tmp_path / "r1.yaml"
    p2 = tmp_path / "p2.yaml"
    r2 = tmp_path / "r2.yaml"
    _run("--input", str(VAULT), "--passport", str(p1), "--rejection-log", str(r1))
    _run("--input", str(VAULT), "--passport", str(p2), "--rejection-log", str(r2))
    assert clean_timestamps(load_yaml(p1)) == clean_timestamps(load_yaml(p2))
    assert clean_timestamps(load_yaml(r1)) == clean_timestamps(load_yaml(r2))


# ---------------------------------------------------------------------------
# P1 — year coercion + authors type validation
# ---------------------------------------------------------------------------

def test_year_leading_4_digit_string_accepted(tmp_path, load_yaml):
    """Convention A: year='2024-01' (leading-year string) is accepted; year extracted as 2024."""
    vault = tmp_path / "v"
    vault.mkdir()
    (vault / "leading_year.md").write_text(
        "---\ncitekey: leading2024\ntitle: T\nauthors:\n  - family: Smith\nyear: '2024-01'\n---\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0
    passport = load_yaml(p)
    assert len(passport["literature_corpus"]) == 1
    assert passport["literature_corpus"][0]["year"] == 2024


def test_year_unparseable_is_rejected(tmp_path, load_yaml):
    """Convention A: year='not-a-year' (non-numeric string) must be rejected with year_unparseable."""
    vault = tmp_path / "v"
    vault.mkdir()
    (vault / "bad_year.md").write_text(
        "---\ncitekey: bad2024\ntitle: T\nauthors:\n  - family: Smith\nyear: 'unknown'\n---\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0
    passport = load_yaml(p)
    assert passport["literature_corpus"] == []
    rejection = load_yaml(r)
    assert len(rejection["rejected"]) == 1
    assert rejection["rejected"][0]["reason"] == "year_unparseable"
    assert rejection["rejected"][0]["source"] == "bad_year.md"


def test_authors_string_parsed_via_semicolon(tmp_path, load_yaml):
    """Convention A: authors as a bare string 'Smith, J; Lee, K' must be parsed into CSL list."""
    vault = tmp_path / "v"
    vault.mkdir()
    (vault / "str_authors.md").write_text(
        "---\ncitekey: smith2024\ntitle: A Study\nauthors: 'Smith, J; Lee, K'\nyear: 2024\n---\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0
    passport = load_yaml(p)
    assert len(passport["literature_corpus"]) == 1
    authors = passport["literature_corpus"][0]["authors"]
    assert isinstance(authors, list)
    assert authors[0]["family"] == "Smith"
    assert authors[1]["family"] == "Lee"


def test_authors_wrong_type_is_rejected(tmp_path, load_yaml):
    """Convention A: authors=42 (invalid type) must be rejected with authors_unparseable."""
    vault = tmp_path / "v"
    vault.mkdir()
    (vault / "bad_authors.md").write_text(
        "---\ncitekey: bad2024\ntitle: T\nauthors: 42\nyear: 2024\n---\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0
    passport = load_yaml(p)
    assert passport["literature_corpus"] == []
    rejection = load_yaml(r)
    assert len(rejection["rejected"]) == 1
    assert rejection["rejected"][0]["reason"] == "authors_unparseable"


# ---------------------------------------------------------------------------
# P2-A — duplicate citekey disambiguation
# ---------------------------------------------------------------------------

def test_duplicate_citekey_convention_a_disambiguated(tmp_path, load_yaml):
    """Two Convention A notes with identical citekeys must both be accepted with suffix disambiguation."""
    vault = tmp_path / "v"
    vault.mkdir()
    (vault / "note1.md").write_text(
        "---\ncitekey: smith2024\ntitle: First Paper\nauthors:\n  - family: Smith\nyear: 2024\n---\n",
        encoding="utf-8",
    )
    (vault / "note2.md").write_text(
        "---\ncitekey: smith2024\ntitle: Second Paper\nauthors:\n  - family: Smith\nyear: 2024\n---\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0
    passport = load_yaml(p)
    assert len(passport["literature_corpus"]) == 2
    keys = {e["citation_key"] for e in passport["literature_corpus"]}
    assert "smith2024" in keys
    assert "smith2024a" in keys


# ---------------------------------------------------------------------------
# P2-B — malformed YAML frontmatter
# ---------------------------------------------------------------------------

def test_malformed_yaml_frontmatter_rejected(tmp_path, load_yaml):
    """File starting with '---' but containing invalid YAML must be rejected, not fall through to Convention B."""
    vault = tmp_path / "v"
    vault.mkdir()
    (vault / "malformed.md").write_text(
        "---\nbad: [unclosed\n---\nbody content here\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0
    passport = load_yaml(p)
    assert passport["literature_corpus"] == []
    rejection = load_yaml(r)
    assert len(rejection["rejected"]) == 1
    rej = rejection["rejected"][0]
    assert rej["reason"] in ("invalid_field_format", "adapter_error")
    assert rej["source"] == "malformed.md"


# ---------------------------------------------------------------------------
# P3 — additional coverage
# ---------------------------------------------------------------------------

def test_empty_file_rejected(tmp_path, load_yaml):
    """Empty .md file must be rejected (no frontmatter, no body, no title/authors/year)."""
    vault = tmp_path / "v"
    vault.mkdir()
    (vault / "empty.md").write_text("", encoding="utf-8")
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0
    passport = load_yaml(p)
    assert passport["literature_corpus"] == []
    rejection = load_yaml(r)
    assert len(rejection["rejected"]) == 1


def test_empty_frontmatter_only_rejected(tmp_path, load_yaml):
    """File with only '---\\n---\\n' (empty frontmatter, no body) must be rejected."""
    vault = tmp_path / "v"
    vault.mkdir()
    (vault / "empty_fm.md").write_text("---\n---\n", encoding="utf-8")
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0
    passport = load_yaml(p)
    assert passport["literature_corpus"] == []


def test_source_pointer_url_encoding_for_spaces(tmp_path, load_yaml):
    """Vault name and filename with spaces must produce valid percent-encoded obsidian:// URI."""
    vault = tmp_path / "My KB"
    vault.mkdir()
    (vault / "Chen 2024.md").write_text(
        "---\ncitekey: chen2024\ntitle: A Study\nauthors:\n  - family: Chen\nyear: 2024\n---\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0
    passport = load_yaml(p)
    entry = passport["literature_corpus"][0]
    assert "My%20KB" in entry["source_pointer"]
    assert "Chen%202024" in entry["source_pointer"]
    assert " " not in entry["source_pointer"]


def test_all_rejected_corpus_produces_empty_passport(tmp_path, load_yaml):
    """All-rejected corpus: passport has empty literature_corpus, rejection_log is populated."""
    vault = tmp_path / "v"
    vault.mkdir()
    (vault / "only_invalid.md").write_text(
        "This file has no frontmatter and no parseable structure.\n",
        encoding="utf-8",
    )
    p = tmp_path / "p.yaml"
    r = tmp_path / "r.yaml"
    res = _run("--input", str(vault), "--passport", str(p), "--rejection-log", str(r))
    assert res.returncode == 0
    passport = load_yaml(p)
    assert passport["literature_corpus"] == []
    rejection = load_yaml(r)
    assert len(rejection["rejected"]) >= 1
    assert rejection["summary"]["total_rejected"] >= 1


# --- v3.10 venue_type frontmatter declaration (spec §3 PR-B item 13) ---

def _import_obsidian():
    import importlib.util
    spec = importlib.util.spec_from_file_location("obsidian_adapter", ADAPTER)
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def test_obsidian_declare_venue_type_valid_frontmatter():
    o = _import_obsidian()
    assert o.declare_venue_type({"venue_type": "journal-article"}) == ("journal-article", "user_declared")
    assert o.declare_venue_type({"venue_type": "dissertation"}) == ("dissertation", "user_declared")


def test_obsidian_declare_venue_type_invalid_or_absent():
    o = _import_obsidian()
    # Invalid enum value → unknown/unknown (never guessed).
    assert o.declare_venue_type({"venue_type": "not-a-real-type"}) == ("unknown", "unknown")
    # Absent → unknown/unknown.
    assert o.declare_venue_type({}) == ("unknown", "unknown")
    assert o.declare_venue_type(None) == ("unknown", "unknown")
    # Explicit unknown → unknown/unknown (provenance unknown, not user_declared).
    assert o.declare_venue_type({"venue_type": "unknown"}) == ("unknown", "unknown")
