from __future__ import annotations
import os
from pathlib import Path

import pytest

from gotta import content
from gotta import target
from gotta.plugins import peer, read, session


def test_read_local_directory_renders_native_listing(tmp_path: Path, capsys) -> None:
    root = tmp_path / "repo"
    root.mkdir()
    (root / ".git").mkdir()
    (root / ".git" / "config").write_text("[core]\n\trepositoryformatversion = 0\n", encoding="utf-8")
    (root / "docs").mkdir()
    (root / "docs" / "README.md").write_text("# Docs\n", encoding="utf-8")

    assert read.main([str(root), "--recursive", "--max-depth", "2"]) == 0

    output = capsys.readouterr().out
    assert f"# Directory: {root.resolve()}" in output
    assert "`.git/`" in output
    assert "`.git/config`" in output
    assert "`docs/`" in output
    assert "`docs/README.md`" in output


def test_read_requires_session_context_for_relative_hidden_file(
    monkeypatch, tmp_path: Path, capsys
) -> None:
    repo = tmp_path / "repo"
    repo.mkdir()
    (repo / ".git").mkdir()
    config = repo / ".git" / "config"
    config.write_text("[remote \"origin\"]\n\turl = git@github.com:acme/widgets.git\n", encoding="utf-8")

    monkeypatch.chdir(tmp_path)
    monkeypatch.setenv("GOTTA_WORK_REPO", str(repo))

    assert read.main([".git/config"]) == 2
    err = capsys.readouterr().err
    assert "requires an active or explicit session root" in err


def test_read_prefers_session_relative_target_over_shell_cwd(
    monkeypatch, tmp_path: Path, capsys
) -> None:
    cwd_root = tmp_path / "cwd"
    session_root = tmp_path / "session"
    cwd_root.mkdir()
    session_root.mkdir()
    (cwd_root / "BRIEF.md").write_text("cwd copy\n", encoding="utf-8")
    (session_root / "BRIEF.md").write_text("session copy\n", encoding="utf-8")

    monkeypatch.chdir(cwd_root)
    monkeypatch.setenv("GOTTA_SESSION_DIR", str(session_root))

    assert read.main(["BRIEF.md"]) == 0
    output = capsys.readouterr().out
    assert "session copy" in output
    assert "cwd copy" not in output


def test_read_can_follow_stored_content_digest(monkeypatch, tmp_path: Path, capsys) -> None:
    session_root = tmp_path / "session"
    dirs = content.ResolvedDirs(session_dir=session_root, content_dir=session_root / "content")
    dirs.session_dir.mkdir(parents=True, exist_ok=True)
    dirs.content_dir.mkdir(parents=True, exist_ok=True)
    result = content.materialize_bytes(
        b"# Stored body\n\nline two\n",
        dirs=dirs,
        preferred_name="stored.md",
        metadata={"plugin": "read", "locator": "demo", "canonical_locator": "demo"},
    )

    monkeypatch.setenv("GOTTA_SESSION_DIR", str(dirs.session_dir))
    monkeypatch.setenv("GOTTA_SESSION_CONTENT_DIR", str(dirs.content_dir))

    assert read.main([result.digest]) == 0
    output = capsys.readouterr().out
    assert "# Stored body" in output


def test_read_can_follow_explicit_content_locator(monkeypatch, tmp_path: Path, capsys) -> None:
    session_root = tmp_path / "session"
    dirs = content.ResolvedDirs(session_dir=session_root, content_dir=session_root / "content")
    dirs.session_dir.mkdir(parents=True, exist_ok=True)
    dirs.content_dir.mkdir(parents=True, exist_ok=True)
    result = content.materialize_bytes(
        b"# Stored body\n\nline two\n",
        dirs=dirs,
        preferred_name="stored.md",
        metadata={"plugin": "read", "locator": "demo", "canonical_locator": "demo"},
    )

    monkeypatch.setenv("GOTTA_SESSION_DIR", str(dirs.session_dir))
    monkeypatch.setenv("GOTTA_SESSION_CONTENT_DIR", str(dirs.content_dir))

    assert read.main([f"content:{result.digest}"]) == 0
    output = capsys.readouterr().out
    assert "# Stored body" in output


def test_read_can_follow_unique_session_artifact_name(monkeypatch, tmp_path: Path, capsys) -> None:
    session_root = tmp_path / "session"
    dirs = content.ResolvedDirs(session_dir=session_root, content_dir=session_root / "content")
    dirs.session_dir.mkdir(parents=True, exist_ok=True)
    dirs.content_dir.mkdir(parents=True, exist_ok=True)
    content.materialize_bytes(
        b"# Search Artifact\n\nbody\n",
        dirs=dirs,
        preferred_name="slack-search-abc.md",
        metadata={"plugin": "slack", "locator": "demo", "canonical_locator": "demo"},
    )

    monkeypatch.setenv("GOTTA_SESSION_DIR", str(dirs.session_dir))
    monkeypatch.setenv("GOTTA_SESSION_CONTENT_DIR", str(dirs.content_dir))

    assert read.main(["slack-search-abc.md"]) == 0
    output = capsys.readouterr().out
    assert "# Search Artifact" in output


def test_read_can_follow_explicit_artifact_locator(monkeypatch, tmp_path: Path, capsys) -> None:
    session_root = tmp_path / "session"
    dirs = content.ResolvedDirs(session_dir=session_root, content_dir=session_root / "content")
    dirs.session_dir.mkdir(parents=True, exist_ok=True)
    dirs.content_dir.mkdir(parents=True, exist_ok=True)
    result = content.materialize_bytes(
        b"# Search Artifact\n\nbody\n",
        dirs=dirs,
        preferred_name="slack-search-abc.md",
        metadata={"plugin": "slack", "locator": "demo", "canonical_locator": "demo"},
    )

    monkeypatch.setenv("GOTTA_SESSION_DIR", str(dirs.session_dir))
    monkeypatch.setenv("GOTTA_SESSION_CONTENT_DIR", str(dirs.content_dir))

    assert read.main([content.artifact_locator("slack-search-abc.md", result.digest)]) == 0
    output = capsys.readouterr().out
    assert "# Search Artifact" in output


def test_read_can_follow_artifact_locator_from_session_root_state_env(
    monkeypatch, tmp_path: Path, capsys
) -> None:
    session_root = tmp_path / "session"
    dirs = content.ResolvedDirs(session_dir=session_root, content_dir=session_root / "content")
    dirs.session_dir.mkdir(parents=True, exist_ok=True)
    dirs.content_dir.mkdir(parents=True, exist_ok=True)
    content.write_state_env(dirs)
    result = content.materialize_bytes(
        b"# Search Artifact\n\nbody\n",
        dirs=dirs,
        preferred_name="slack-search-abc.md",
        metadata={"plugin": "slack", "locator": "demo", "canonical_locator": "demo"},
    )

    monkeypatch.delenv("GOTTA_SESSION_DIR", raising=False)
    monkeypatch.delenv("GOTTA_SESSION_CONTENT_DIR", raising=False)
    monkeypatch.chdir(dirs.session_dir)

    assert read.main([content.artifact_locator("slack-search-abc.md", result.digest)]) == 0
    output = capsys.readouterr().out
    assert "# Search Artifact" in output


def test_read_can_follow_artifact_locator_from_peer_root_state_env(
    monkeypatch, tmp_path: Path, capsys
) -> None:
    session_root = tmp_path / "session"
    dirs = content.ResolvedDirs(session_dir=session_root, content_dir=session_root / "content")
    dirs.session_dir.mkdir(parents=True, exist_ok=True)
    dirs.content_dir.mkdir(parents=True, exist_ok=True)
    content.write_state_env(dirs)
    result = content.materialize_bytes(
        b"# Search Artifact\n\nbody\n",
        dirs=dirs,
        preferred_name="slack-search-abc.md",
        metadata={"plugin": "slack", "locator": "demo", "canonical_locator": "demo"},
    )
    peer_root = session_root / "peers" / "claude"
    peer_state = peer_root / "state"
    peer_state.mkdir(parents=True, exist_ok=True)
    (peer_state / "env").write_text(
        "\n".join(
            [
                f"export GOTTA_SESSION_DIR='{peer_root}'",
                f"export GOTTA_SESSION_CONTENT_DIR='{dirs.content_dir}'",
            ]
        )
        + "\n",
        encoding="utf-8",
    )

    monkeypatch.delenv("GOTTA_SESSION_DIR", raising=False)
    monkeypatch.delenv("GOTTA_SESSION_CONTENT_DIR", raising=False)
    monkeypatch.chdir(peer_root)

    assert read.main([content.artifact_locator("slack-search-abc.md", result.digest)]) == 0
    output = capsys.readouterr().out
    assert "# Search Artifact" in output


def test_read_missing_peer_local_surface_reports_missing_local_path(
    monkeypatch, tmp_path: Path, capsys
) -> None:
    session_root = tmp_path / "session"
    peer_root = tmp_path / "session-claude"
    session_root.mkdir()
    peer_root.mkdir()
    (session_root / "peers").mkdir()
    (session_root / "peers" / "claude").symlink_to(
        os.path.relpath(peer_root, start=(session_root / "peers")),
    )

    monkeypatch.setenv("GOTTA_SESSION_DIR", str(session_root))
    monkeypatch.setenv("GOTTA_SESSION_CONTENT_DIR", str(session_root / "content"))

    assert read.main(["peers/claude/WANT.md"]) == 2
    err = capsys.readouterr().err
    assert "local target 'peers/claude/WANT.md' does not exist" in err
    assert "unsupported target" not in err


def test_read_missing_peer_goal_surface_reports_missing_local_path(
    monkeypatch, tmp_path: Path, capsys
) -> None:
    session_root = tmp_path / "session"
    peer_root = tmp_path / "session-claude"
    session_root.mkdir()
    peer_root.mkdir()
    (session_root / "peers").mkdir()
    (session_root / "peers" / "claude").symlink_to(
        os.path.relpath(peer_root, start=(session_root / "peers")),
    )

    monkeypatch.setenv("GOTTA_SESSION_DIR", str(session_root))
    monkeypatch.setenv("GOTTA_SESSION_CONTENT_DIR", str(session_root / "content"))

    assert read.main(["peers/claude/GOAL.md"]) == 2
    err = capsys.readouterr().err
    assert "local target 'peers/claude/GOAL.md' does not exist" in err
    assert "unsupported target" not in err


def test_read_seeded_peer_local_charter_surfaces(monkeypatch, tmp_path: Path, capsys) -> None:
    root = tmp_path / "work-root"

    assert session.main(["init", "--session", str(root)]) == 0
    capsys.readouterr()
    (root / "WANT.md").write_text("# Want\n\nReal intent.\n", encoding="utf-8")
    (root / "GOAL.md").write_text("# Goal\n\nReal goal.\n", encoding="utf-8")

    assert peer.main(["with", "Claude", "--session", str(root)]) == 0
    capsys.readouterr()

    monkeypatch.setenv("GOTTA_SESSION_DIR", str(root))
    monkeypatch.setenv("GOTTA_SESSION_CONTENT_DIR", str(root / "content"))

    assert read.main(["peers/claude/WANT.md"]) == 0
    want_output = capsys.readouterr().out
    assert "# Peer Want Placeholder" in want_output

    assert read.main(["peers/claude/GOAL.md"]) == 0
    goal_output = capsys.readouterr().out
    assert "# Seed Peer Goal Placeholder" in goal_output


def test_read_does_not_materialize_local_artifact_rereads(monkeypatch, tmp_path: Path) -> None:
    session_root = tmp_path / "session"
    dirs = content.ResolvedDirs(session_dir=session_root, content_dir=session_root / "content")
    dirs.session_dir.mkdir(parents=True, exist_ok=True)
    dirs.content_dir.mkdir(parents=True, exist_ok=True)
    result = content.materialize_bytes(
        b"# Search Artifact\n\nbody\n",
        dirs=dirs,
        preferred_name="slack-search-abc.md",
        metadata={"plugin": "slack", "locator": "demo", "canonical_locator": "demo"},
    )

    monkeypatch.setenv("GOTTA_SESSION_DIR", str(dirs.session_dir))
    monkeypatch.setenv("GOTTA_SESSION_CONTENT_DIR", str(dirs.content_dir))

    assert not target.should_materialize(
        [content.artifact_locator("slack-search-abc.md", result.digest), "--head", "2"]
    )


def test_read_ambiguous_session_artifact_name_suggests_explicit_artifact_locators(
    monkeypatch, tmp_path: Path
) -> None:
    session_root = tmp_path / "session"
    dirs = content.ResolvedDirs(session_dir=session_root, content_dir=session_root / "content")
    dirs.session_dir.mkdir(parents=True, exist_ok=True)
    dirs.content_dir.mkdir(parents=True, exist_ok=True)
    first = content.materialize_bytes(
        b"# Search Artifact A\n",
        dirs=dirs,
        preferred_name="slack-search-abc.md",
        metadata={"plugin": "slack", "locator": "demo-a", "canonical_locator": "demo-a"},
    )
    second = content.materialize_bytes(
        b"# Search Artifact B\n",
        dirs=dirs,
        preferred_name="slack-search-abc.md",
        metadata={"plugin": "slack", "locator": "demo-b", "canonical_locator": "demo-b"},
    )

    monkeypatch.setenv("GOTTA_SESSION_DIR", str(dirs.session_dir))
    monkeypatch.setenv("GOTTA_SESSION_CONTENT_DIR", str(dirs.content_dir))

    with pytest.raises(SystemExit) as excinfo:
        read.main(["slack-search-abc.md"])
    message = str(excinfo.value)
    assert "artifact:slack-search-abc.md@" in message
    assert content.artifact_locator("slack-search-abc.md", first.digest) in message
    assert content.artifact_locator("slack-search-abc.md", second.digest) in message


def test_read_supports_bounded_local_views(tmp_path: Path, capsys) -> None:
    target = tmp_path / "notes.md"
    target.write_text(
        "# Intro\n\nline 1\nline 2\n## Details\n\na\nb\nc\n",
        encoding="utf-8",
    )

    assert read.main([str(target), "--section", "Details", "--head", "2"]) == 0
    output = capsys.readouterr().out
    assert "## Details" in output
    assert "a" in output
    assert "b" not in output


def test_read_supports_bounded_provider_views(monkeypatch, capsys) -> None:
    monkeypatch.setattr(
        read,
        "resolve_read_target",
        lambda argv: target.ReadTarget(
            request=target.parse_args(argv),
            kind="routed",
            path=None,
            routed_plugin="github",
            routed_argv=["fake"],
            canonical_locator="https://github.com/acme/widgets",
            preferred_name="widgets.md",
            should_materialize=True,
        ),
    )

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        print("# Title")
        print("")
        print("line 1")
        print("line 2")
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["https://github.com/acme/widgets", "--head", "3"]) == 0
    output = capsys.readouterr().out
    assert "# Title" in output
    assert "line 1" in output
    assert "line 2" not in output


def test_read_passes_provider_flags_through_for_routed_targets(monkeypatch) -> None:
    resolved = target.resolve_read_target(
        ["--limit", "10", "https://github.com/acme/widgets/commits/main"]
    )

    assert resolved.kind == "routed"
    assert resolved.routed_plugin == "github"
    assert resolved.routed_argv == [
        "--limit",
        "10",
        "https://github.com/acme/widgets/commits/main",
    ]


def test_read_passes_provider_flags_through_after_routed_target(monkeypatch) -> None:
    resolved = target.resolve_read_target(
        ["https://github.com/acme/widgets/commits/main", "--limit", "10"]
    )

    assert resolved.kind == "routed"
    assert resolved.routed_plugin == "github"
    assert resolved.routed_argv == [
        "https://github.com/acme/widgets/commits/main",
        "--limit",
        "10",
    ]


def test_read_uses_provider_normalized_fragmentless_url_for_routed_targets() -> None:
    resolved = target.resolve_read_target(
        ["https://docs.google.com/spreadsheets/d/sheet-123/edit#gid=0"]
    )

    assert resolved.kind == "routed"
    assert resolved.routed_plugin == "gsheets"
    assert resolved.routed_argv == [
        "get",
        "https://docs.google.com/spreadsheets/d/sheet-123/edit",
    ]


def test_read_preserves_github_fragment_hint_while_canonicalizing_identity() -> None:
    resolved = target.resolve_read_target(["https://github.com/acme/widgets#readme"])

    assert resolved.kind == "routed"
    assert resolved.routed_plugin == "github"
    assert resolved.routed_argv == ["https://github.com/acme/widgets#readme"]
    assert resolved.canonical_locator == "https://github.com/acme/widgets"


def test_read_delegates_github_commit_history_limit_after_target(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["https://github.com/acme/widgets/commits/main", "--limit", "10"]) == 0
    assert seen == [
        (
            "github",
            ["https://github.com/acme/widgets/commits/main", "--limit", "10"],
        )
    ]


def test_read_can_follow_canonical_jira_locator(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["jira:PROJ-3960"]) == 0
    assert seen == [("jira", ["get", "PROJ-3960"])]


def test_read_can_follow_canonical_search_locator(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["jira:search Architecture"]) == 0
    assert seen == [("jira", ["search", "Architecture"])]


def test_read_can_follow_canonical_granola_document_locator(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["granola:11111111-1111-1111-1111-111111111111"]) == 0
    assert seen == [("granola", ["get", "11111111-1111-1111-1111-111111111111"])]


def test_read_can_follow_canonical_granola_search_locator(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["granola:search --limit 5 latency"]) == 0
    assert seen == [("granola", ["search", "--limit", "5", "latency"])]


def test_read_can_follow_canonical_granola_transcript_search_locator(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["granola:search-transcript --all latency"]) == 0
    assert seen == [("granola", ["search-transcript", "--all", "latency"])]


def test_read_can_follow_canonical_granola_transcript_locator(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["granola:transcript 11111111-1111-1111-1111-111111111111"]) == 0
    assert seen == [("granola", ["transcript", "11111111-1111-1111-1111-111111111111"])]


def test_read_can_follow_canonical_search_locator_with_flags(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert (
        read.main(["slack:search --workspace example-workspace --source archive --limit 10 --output markdown ABC"])
        == 0
    )
    assert seen == [
        (
            "slack",
            ["search", "--workspace", "example-workspace", "--source", "archive", "--limit", "10", "--output", "markdown", "ABC"],
        )
    ]


def test_read_can_follow_canonical_search_locator_with_equals_flags_unquoted(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["jira:search", "--limit=10", "Architecture"]) == 0
    assert seen == [("jira", ["search", "--limit", "10", "Architecture"])]


def test_read_can_follow_canonical_search_locator_with_flags_after_query(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["slack:search ABC reboot --workspace example-workspace --limit 10"]) == 0
    assert seen == [
        (
            "slack",
            ["search", "--workspace", "example-workspace", "--limit", "10", "ABC reboot"],
        )
    ]


def test_read_can_follow_canonical_github_search_locator(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["github:search --type pr --repo acme/widgets ABC"]) == 0
    assert seen == [("github", ["search", "--type", "pr", "--repo", "acme/widgets", "ABC"])]


def test_read_can_follow_canonical_global_github_search_locator(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["github:search --global ABC"]) == 0
    assert seen == [("github", ["search", "--global", "ABC"])]


def test_read_can_follow_slack_workspace_locator(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["slack:example-workspace"]) == 0
    assert seen == [("slack", ["status", "--workspace", "example-workspace", "--output", "summary"])]


def test_read_joins_unquoted_multiword_search_locator(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["jira:search", "ABC", "reboot"]) == 0
    assert seen == [("jira", ["search", "ABC reboot"])]


def test_read_accepts_option_like_locator_tails_after_end_of_options(monkeypatch) -> None:
    seen: list[tuple[str, list[str]]] = []

    def fake_delegate(plugin: str, argv: list[str]) -> int:
        seen.append((plugin, argv))
        return 0

    monkeypatch.setattr(read, "delegate", fake_delegate)

    assert read.main(["--", "jira:search", "ABC", "--head", "180"]) == 0
    assert seen == [("jira", ["search", "ABC --head 180"])]
