"""Integration tests for gwtm.core — business logic with real git repos."""

from pathlib import Path

import pytest

from gwtm.core import (
    CarriedFile,
    CdResult,
    NewWorktreeResult,
    carry_changes_to,
    carry_files,
    create_worktree,
    get_worktree_statuses,
    remove_worktree,
    resolve_worktree_path,
    get_worktree_names,
    sync_worktree,
    get_repo_status,
    clean_worktrees,
)
from gwtm.utils import (
    DirtyWorktreeError,
    GwtError,
    WorktreeExistsError,
    WorktreeNotFoundError,
    run_git,
)


class TestCreateWorktree:
    def test_creates_worktree(self, managed_repo: Path) -> None:
        result = create_worktree("feature-1", cwd=managed_repo)

        assert isinstance(result, NewWorktreeResult)
        assert result.name == "feature-1"
        assert result.branch == "wt/feature-1"
        assert result.path.exists()
        assert (result.path / "README.md").exists()  # File from initial commit

    def test_custom_base_branch(self, managed_repo: Path) -> None:
        from gwtm.utils import run_git
        # Create a develop branch
        run_git("branch", "develop", cwd=managed_repo)

        result = create_worktree("feature-2", base_branch="develop", cwd=managed_repo)
        assert result.base_branch == "develop"

    def test_rejects_duplicate_name(self, managed_repo: Path) -> None:
        create_worktree("feature-dup", cwd=managed_repo)

        with pytest.raises(WorktreeExistsError):
            create_worktree("feature-dup", cwd=managed_repo)

    def test_rejects_invalid_name(self, managed_repo: Path) -> None:
        with pytest.raises(ValueError, match="Invalid worktree name"):
            create_worktree("feat/slash", cwd=managed_repo)

    def test_rejects_nonexistent_base(self, managed_repo: Path) -> None:
        with pytest.raises(GwtError, match="does not exist"):
            create_worktree("feature-3", base_branch="nonexistent", cwd=managed_repo)


class TestGetWorktreeStatuses:
    def test_lists_main_only(self, managed_repo: Path) -> None:
        statuses = get_worktree_statuses(cwd=managed_repo)
        repo_name = managed_repo.name

        assert repo_name in statuses
        assert len(statuses[repo_name]) >= 1
        assert statuses[repo_name][0].is_main

    def test_lists_created_worktrees(self, managed_repo: Path) -> None:
        create_worktree("feat-ls", cwd=managed_repo)
        statuses = get_worktree_statuses(cwd=managed_repo)
        repo_name = managed_repo.name

        names = [s.name for s in statuses[repo_name]]
        assert "feat-ls" in names

    def test_detects_dirty_worktree(self, managed_repo: Path) -> None:
        result = create_worktree("dirty-wt", cwd=managed_repo)
        (result.path / "new_file.txt").write_text("uncommitted\n")

        statuses = get_worktree_statuses(cwd=managed_repo)
        repo_name = managed_repo.name

        dirty_wt = next(s for s in statuses[repo_name] if s.name == "dirty-wt")
        assert dirty_wt.dirty > 0


class TestResolveWorktreePath:
    def test_resolves_main(self, managed_repo: Path) -> None:
        result = resolve_worktree_path("main", cwd=managed_repo)
        assert result.found
        assert result.path == managed_repo
        assert result.name == "main"

    def test_resolves_exact_name(self, managed_repo: Path) -> None:
        create_worktree("exact-match", cwd=managed_repo)
        result = resolve_worktree_path("exact-match", cwd=managed_repo)
        assert result.found
        assert result.name == "exact-match"

    def test_resolves_prefix(self, managed_repo: Path) -> None:
        create_worktree("redis-session", cwd=managed_repo)
        result = resolve_worktree_path("redis", cwd=managed_repo)
        assert result.found
        assert result.name == "redis-session"

    def test_resolves_substring(self, managed_repo: Path) -> None:
        create_worktree("auth-refactor", cwd=managed_repo)
        result = resolve_worktree_path("refactor", cwd=managed_repo)
        assert result.found
        assert result.name == "auth-refactor"

    def test_not_found_with_suggestions(self, managed_repo: Path) -> None:
        create_worktree("redis-session", cwd=managed_repo)
        result = resolve_worktree_path("rediz", cwd=managed_repo)
        assert not result.found
        assert "redis-session" in result.suggestions

    def test_ambiguous_prefix_gives_suggestions(self, managed_repo: Path) -> None:
        create_worktree("feat-auth", cwd=managed_repo)
        create_worktree("feat-cache", cwd=managed_repo)
        result = resolve_worktree_path("feat", cwd=managed_repo)
        # Multiple prefix matches → not found, gives suggestions
        assert not result.found
        assert len(result.suggestions) >= 2


class TestRemoveWorktree:
    def test_removes_worktree(self, managed_repo: Path) -> None:
        wt = create_worktree("to-remove", cwd=managed_repo)
        assert wt.path.exists()

        result = remove_worktree("to-remove", cwd=managed_repo)
        assert not wt.path.exists()
        assert result.branch_deleted

    def test_keeps_branch_when_asked(self, managed_repo: Path) -> None:
        from gwtm.utils import run_git

        create_worktree("keep-branch", cwd=managed_repo)
        result = remove_worktree("keep-branch", keep_branch=True, cwd=managed_repo)
        assert not result.branch_deleted

        # Branch should still exist
        branches = run_git("branch", cwd=managed_repo)
        assert "wt/keep-branch" in branches

    def test_rejects_dirty_without_force(self, managed_repo: Path) -> None:
        wt = create_worktree("dirty-rm", cwd=managed_repo)
        (wt.path / "dirty.txt").write_text("uncommitted\n")

        with pytest.raises(DirtyWorktreeError):
            remove_worktree("dirty-rm", cwd=managed_repo)

    def test_force_removes_dirty(self, managed_repo: Path) -> None:
        wt = create_worktree("force-rm", cwd=managed_repo)
        (wt.path / "dirty.txt").write_text("uncommitted\n")

        result = remove_worktree("force-rm", force=True, cwd=managed_repo)
        assert not wt.path.exists()

    def test_raises_for_nonexistent(self, managed_repo: Path) -> None:
        with pytest.raises(WorktreeNotFoundError):
            remove_worktree("nonexistent", cwd=managed_repo)


class TestSyncWorktree:
    def test_sync_specific_worktree(self, managed_repo: Path) -> None:
        create_worktree("sync-test", cwd=managed_repo)
        results = sync_worktree(name="sync-test", cwd=managed_repo)
        assert len(results) == 1
        assert results[0].success
        assert results[0].name == "sync-test"

    def test_sync_all(self, managed_repo: Path) -> None:
        create_worktree("sync-a", cwd=managed_repo)
        create_worktree("sync-b", cwd=managed_repo)
        results = sync_worktree(all_worktrees=True, cwd=managed_repo)
        assert len(results) == 2
        assert all(r.success for r in results)

    def test_sync_nonexistent(self, managed_repo: Path) -> None:
        results = sync_worktree(name="nope", cwd=managed_repo)
        assert len(results) == 1
        assert not results[0].success


class TestGetRepoStatus:
    def test_returns_repo_info(self, managed_repo: Path) -> None:
        repo_name, main_path, statuses = get_repo_status(cwd=managed_repo)
        assert repo_name == "test-repo"
        assert main_path == managed_repo
        assert len(statuses) >= 1

    def test_includes_worktrees(self, managed_repo: Path) -> None:
        create_worktree("status-test", cwd=managed_repo)
        _, _, statuses = get_repo_status(cwd=managed_repo)
        names = [s.name for s in statuses]
        assert "status-test" in names


class TestCleanWorktrees:
    def test_no_candidates_when_none_merged(self, managed_repo: Path) -> None:
        from gwtm.utils import run_git

        wt = create_worktree("not-merged", cwd=managed_repo)
        # Make a commit so the branch diverges from main
        (wt.path / "feature.txt").write_text("feature\n")
        run_git("add", ".", cwd=wt.path)
        run_git("commit", "-m", "feature", cwd=wt.path)

        candidates = clean_worktrees(dry_run=True, cwd=managed_repo)
        assert len(candidates) == 0

    def test_finds_merged_candidates(self, managed_repo: Path) -> None:
        # A worktree with no extra commits is already "merged" into its base
        create_worktree("already-merged", cwd=managed_repo)
        candidates = clean_worktrees(dry_run=True, cwd=managed_repo)
        names = [c.name for c in candidates]
        assert "already-merged" in names

    def test_dry_run_does_not_remove(self, managed_repo: Path) -> None:
        wt = create_worktree("dry-run-test", cwd=managed_repo)
        clean_worktrees(dry_run=True, cwd=managed_repo)
        assert wt.path.exists()

    def test_clean_removes_merged(self, managed_repo: Path) -> None:
        wt = create_worktree("clean-me", cwd=managed_repo)
        assert wt.path.exists()

        clean_worktrees(dry_run=False, cwd=managed_repo)
        assert not wt.path.exists()


class TestGetWorktreeNames:
    def test_empty_when_no_worktrees(self, managed_repo: Path) -> None:
        names = get_worktree_names(cwd=managed_repo)
        assert names == []

    def test_lists_created_names(self, managed_repo: Path) -> None:
        create_worktree("wt-a", cwd=managed_repo)
        create_worktree("wt-b", cwd=managed_repo)
        names = get_worktree_names(cwd=managed_repo)
        assert names == ["wt-a", "wt-b"]


class TestCarryFiles:
    def test_symlinks_directory(self, tmp_path: Path) -> None:
        src = tmp_path / "src"
        dst = tmp_path / "dst"
        src.mkdir()
        dst.mkdir()

        ai_dir = src / ".ai"
        ai_dir.mkdir()
        (ai_dir / "notes.md").write_text("context")
        (ai_dir / "sub").mkdir()
        (ai_dir / "sub" / "deep.txt").write_text("deep")

        result = carry_files(src, dst, [".ai/"])
        assert len(result) == 1
        assert result[0].is_dir
        assert (dst / ".ai").is_symlink()
        assert (dst / ".ai" / "notes.md").read_text() == "context"
        assert (dst / ".ai" / "sub" / "deep.txt").read_text() == "deep"

    def test_symlinks_single_file(self, tmp_path: Path) -> None:
        src = tmp_path / "src"
        dst = tmp_path / "dst"
        src.mkdir()
        dst.mkdir()
        (src / ".env.local").write_text("SECRET=1")

        result = carry_files(src, dst, [".env.local"])
        assert len(result) == 1
        assert not result[0].is_dir
        assert (dst / ".env.local").is_symlink()
        assert (dst / ".env.local").read_text() == "SECRET=1"

    def test_skips_missing_pattern(self, tmp_path: Path) -> None:
        src = tmp_path / "src"
        dst = tmp_path / "dst"
        src.mkdir()
        dst.mkdir()

        result = carry_files(src, dst, [".nonexistent/"])
        assert result == []

    def test_symlink_shares_changes(self, tmp_path: Path) -> None:
        """Edits in source reflect in dest and vice versa."""
        src = tmp_path / "src"
        dst = tmp_path / "dst"
        src.mkdir()
        dst.mkdir()

        (src / ".ai").mkdir()
        (src / ".ai" / "notes.md").write_text("original")

        carry_files(src, dst, [".ai/"])

        # Edit via source → visible in dest
        (src / ".ai" / "notes.md").write_text("updated")
        assert (dst / ".ai" / "notes.md").read_text() == "updated"

        # Edit via dest → visible in source
        (dst / ".ai" / "new.md").write_text("new file")
        assert (src / ".ai" / "new.md").read_text() == "new file"

    def test_multiple_patterns(self, tmp_path: Path) -> None:
        src = tmp_path / "src"
        dst = tmp_path / "dst"
        src.mkdir()
        dst.mkdir()

        (src / ".ai").mkdir()
        (src / ".ai" / "notes.md").write_text("ai")
        (src / ".cursor").mkdir()
        (src / ".cursor" / "config").write_text("cursor")

        result = carry_files(src, dst, [".ai/", ".cursor/"])
        assert len(result) == 2
        assert (dst / ".ai").is_symlink()
        assert (dst / ".cursor").is_symlink()

    def test_empty_patterns(self, tmp_path: Path) -> None:
        src = tmp_path / "src"
        dst = tmp_path / "dst"
        src.mkdir()
        dst.mkdir()

        result = carry_files(src, dst, [])
        assert result == []


class TestCarryChanges:
    def test_carries_staged_changes(self, temp_git_repo: Path, managed_repo: Path) -> None:
        wt = create_worktree("carry-staged", cwd=managed_repo)

        # Stage a new file in the source
        (managed_repo / "staged.txt").write_text("staged content")
        run_git("add", "staged.txt", cwd=managed_repo)

        applied = carry_changes_to(managed_repo, wt.path)
        assert applied
        assert (wt.path / "staged.txt").exists()

    def test_carries_unstaged_changes(self, temp_git_repo: Path, managed_repo: Path) -> None:
        wt = create_worktree("carry-unstaged", cwd=managed_repo)

        # Modify an existing tracked file without staging
        (managed_repo / "README.md").write_text("modified content\n")

        applied = carry_changes_to(managed_repo, wt.path)
        assert applied
        assert (wt.path / "README.md").read_text() == "modified content\n"

    def test_no_changes_is_noop(self, temp_git_repo: Path, managed_repo: Path) -> None:
        wt = create_worktree("carry-noop", cwd=managed_repo)

        applied = carry_changes_to(managed_repo, wt.path)
        assert not applied


class TestCreateWorktreeWithCarry:
    def test_carry_patterns_from_config(
        self, managed_repo: Path, monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        # Set up config with carry_patterns
        import gwtm.config as config_mod
        import yaml

        config_file = config_mod.GLOBAL_CONFIG_FILE
        data = yaml.safe_load(config_file.read_text())
        data["carry_patterns"] = [".ai/"]
        config_file.write_text(yaml.dump(data))

        # Create .ai/ in source
        (managed_repo / ".ai").mkdir()
        (managed_repo / ".ai" / "context.md").write_text("context")

        result = create_worktree("carry-config", cwd=managed_repo)
        assert len(result.carried_files) == 1
        assert (result.path / ".ai" / "context.md").exists()

    def test_carry_cli_overrides_config(
        self, managed_repo: Path, monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        import gwtm.config as config_mod
        import yaml

        # Config says carry .ai/
        config_file = config_mod.GLOBAL_CONFIG_FILE
        data = yaml.safe_load(config_file.read_text())
        data["carry_patterns"] = [".ai/"]
        config_file.write_text(yaml.dump(data))

        # Create both .ai/ and .cursor/
        (managed_repo / ".ai").mkdir()
        (managed_repo / ".ai" / "notes.md").write_text("ai")
        (managed_repo / ".cursor").mkdir()
        (managed_repo / ".cursor" / "config").write_text("cursor")

        # CLI explicitly asks for .cursor/ only
        result = create_worktree("carry-override", carry=[".cursor/"], cwd=managed_repo)
        assert any(cf.relative_path == ".cursor" for cf in result.carried_files)
        assert not (result.path / ".ai").exists()

    def test_no_carry_skips_everything(
        self, managed_repo: Path, monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        import gwtm.config as config_mod
        import yaml

        config_file = config_mod.GLOBAL_CONFIG_FILE
        data = yaml.safe_load(config_file.read_text())
        data["carry_patterns"] = [".ai/"]
        data["carry_changes"] = True
        config_file.write_text(yaml.dump(data))

        (managed_repo / ".ai").mkdir()
        (managed_repo / ".ai" / "notes.md").write_text("ai")
        (managed_repo / "README.md").write_text("modified\n")

        result = create_worktree("no-carry-test", no_carry=True, cwd=managed_repo)
        assert result.carried_files == []
        assert not result.carried_changes
        assert not (result.path / ".ai").exists()

    def test_carry_changes_flag(self, managed_repo: Path) -> None:
        (managed_repo / "README.md").write_text("wip change\n")

        result = create_worktree("carry-changes-test", do_carry_changes=True, cwd=managed_repo)
        assert result.carried_changes
        assert (result.path / "README.md").read_text() == "wip change\n"

    def test_warns_unignored_carried(self, managed_repo: Path) -> None:
        # .notignored/ is NOT in .gitignore
        (managed_repo / ".notignored").mkdir()
        (managed_repo / ".notignored" / "data.txt").write_text("data")

        result = create_worktree("unignored-test", carry=[".notignored/"], cwd=managed_repo)
        assert len(result.carried_files) == 1
        assert ".notignored" in result.unignored_carried

    def test_no_warning_for_ignored_carried(self, managed_repo: Path) -> None:
        # Use .ai (no trailing slash) — git treats symlinks as files,
        # so .ai/ won't match a symlink even if it points to a directory
        (managed_repo / ".gitignore").write_text(".ai\n")
        run_git("add", ".gitignore", cwd=managed_repo)
        run_git("commit", "-m", "add gitignore", cwd=managed_repo)

        (managed_repo / ".ai").mkdir()
        (managed_repo / ".ai" / "notes.md").write_text("context")

        result = create_worktree("ignored-test", carry=[".ai/"], cwd=managed_repo)
        assert len(result.carried_files) == 1
        assert result.unignored_carried == []
