"""Tests for git automation helpers."""

from __future__ import annotations

import subprocess
from importlib.resources import files
from pathlib import Path
from typing import Any

import pytest

from dotpromptz.errors import GhCliError, GitError
from dotpromptz.gitops import (
    PushResult,
    build_commit_message_from_staged,
    commit_and_push,
    ensure_gh_authenticated,
    install_workflow,
    submit_prompt,
    sync_repo,
)


def _cp(
    *,
    returncode: int = 0,
    stdout: str = '',
    stderr: str = '',
) -> subprocess.CompletedProcess[str]:
    return subprocess.CompletedProcess(
        args=['mock'],
        returncode=returncode,
        stdout=stdout,
        stderr=stderr,
    )


def test_sync_repo_uses_fast_forward_only(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[list[str]] = []

    def _fake_run(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        calls.append(command)
        key = tuple(command)
        mapping = {
            ('git', 'rev-parse', '--show-toplevel'): _cp(stdout='/repo\n'),
            ('git', 'symbolic-ref', '--quiet', '--short', 'HEAD'): _cp(stdout='main\n'),
            ('git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'): _cp(stdout='origin/main\n'),
            ('git', 'fetch', '--prune'): _cp(),
            ('git', 'pull', '--ff-only'): _cp(),
        }
        if key not in mapping:
            raise AssertionError(f'unexpected command: {command}')
        return mapping[key]

    monkeypatch.setattr('dotpromptz.gitops.subprocess.run', _fake_run)
    sync_repo(cwd=Path('/repo'))

    assert ['git', 'pull', '--ff-only'] in calls
    assert not any('rebase' in item for command in calls for item in command)
    assert not any('merge' in item for command in calls for item in command)


def test_sync_repo_non_fast_forward_raises(monkeypatch: pytest.MonkeyPatch) -> None:
    def _fake_run(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        key = tuple(command)
        mapping = {
            ('git', 'rev-parse', '--show-toplevel'): _cp(stdout='/repo\n'),
            ('git', 'symbolic-ref', '--quiet', '--short', 'HEAD'): _cp(stdout='main\n'),
            ('git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'): _cp(stdout='origin/main\n'),
            ('git', 'fetch', '--prune'): _cp(),
            ('git', 'pull', '--ff-only'): _cp(
                returncode=1,
                stderr='fatal: Not possible to fast-forward, aborting.',
            ),
        }
        if key not in mapping:
            raise AssertionError(f'unexpected command: {command}')
        return mapping[key]

    monkeypatch.setattr('dotpromptz.gitops.subprocess.run', _fake_run)

    with pytest.raises(GitError) as exc_info:
        sync_repo(cwd=Path('/repo'))

    assert exc_info.value.code == 'git_sync_ff_only_failed'


def test_build_commit_message_with_rename(monkeypatch: pytest.MonkeyPatch) -> None:
    diff_output = 'R100\told.txt\tnew.txt\nA\tfoo.prompt\n'

    def _fake_run(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        assert command == ['git', 'diff', '--cached', '--name-status']
        return _cp(stdout=diff_output)

    monkeypatch.setattr('dotpromptz.gitops.subprocess.run', _fake_run)

    message = build_commit_message_from_staged(Path('/repo'))
    assert message.startswith('chore(prompt): ')
    assert 'R:old.txt->new.txt' in message
    assert 'A:foo.prompt' in message


def test_build_commit_message_truncates_by_total_length(monkeypatch: pytest.MonkeyPatch) -> None:
    diff_output = '\n'.join(f'M\tvery/long/path/to/file_{index}_{"x" * 24}.txt' for index in range(30))

    def _fake_run(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        assert command == ['git', 'diff', '--cached', '--name-status']
        return _cp(stdout=diff_output)

    monkeypatch.setattr('dotpromptz.gitops.subprocess.run', _fake_run)

    message = build_commit_message_from_staged(Path('/repo'))
    assert message.startswith('chore(prompt): ')
    assert len(message) == 180
    assert message.endswith('...')


def test_commit_and_push_retries_with_ff_only(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[list[str]] = []
    push_count = 0

    monkeypatch.setattr('dotpromptz.gitops.resolve_repo_root', lambda cwd=None: Path('/repo'))
    monkeypatch.setattr('dotpromptz.gitops.sync_repo', lambda cwd=None: None)

    def _fake_run(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        nonlocal push_count
        calls.append(command)

        if command == ['git', 'add', '.']:
            return _cp()
        if command == ['git', 'diff', '--cached', '--quiet']:
            return _cp(returncode=1)
        if command == ['git', 'diff', '--cached', '--name-status']:
            return _cp(stdout='M\tfoo.prompt\n')
        if command[:3] == ['git', 'commit', '-m']:
            return _cp()
        if command == ['git', 'rev-parse', 'HEAD']:
            return _cp(stdout='abc123\n')
        if command == ['git', 'push']:
            push_count += 1
            if push_count == 1:
                return _cp(returncode=1, stderr='rejected')
            return _cp()
        if command == ['git', 'fetch', '--prune']:
            return _cp()
        if command == ['git', 'pull', '--ff-only']:
            return _cp()
        raise AssertionError(f'unexpected command: {command}')

    monkeypatch.setattr('dotpromptz.gitops.subprocess.run', _fake_run)

    result = commit_and_push(cwd=Path('/repo'))

    assert result.committed is True
    assert result.commit_sha == 'abc123'
    assert ['git', 'pull', '--ff-only'] in calls
    assert not any('rebase' in item for command in calls for item in command)
    assert not any('merge' in item for command in calls for item in command)


def test_commit_and_push_without_changes(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[list[str]] = []

    monkeypatch.setattr('dotpromptz.gitops.resolve_repo_root', lambda cwd=None: Path('/repo'))
    monkeypatch.setattr('dotpromptz.gitops.sync_repo', lambda cwd=None: None)

    def _fake_run(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        calls.append(command)
        if command == ['git', 'add', '.']:
            return _cp()
        if command == ['git', 'diff', '--cached', '--quiet']:
            return _cp(returncode=0)
        raise AssertionError(f'unexpected command: {command}')

    monkeypatch.setattr('dotpromptz.gitops.subprocess.run', _fake_run)

    result = commit_and_push(cwd=Path('/repo'))

    assert result == PushResult(committed=False, commit_sha=None, commit_message=None)
    assert ['git', 'add', '.'] in calls
    assert not any(command[:2] == ['git', 'commit'] for command in calls)


def test_ensure_gh_authenticated_raises_when_not_logged_in(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr('dotpromptz.gitops.resolve_repo_root', lambda cwd=None: Path('/repo'))

    def _fake_run(command: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
        if command == ['gh', '--version']:
            return _cp(stdout='gh version 2.0\n')
        if command == ['gh', 'auth', 'status']:
            return _cp(returncode=1, stderr='You are not logged into any GitHub hosts')
        raise AssertionError(f'unexpected command: {command}')

    monkeypatch.setattr('dotpromptz.gitops.subprocess.run', _fake_run)

    with pytest.raises(GhCliError) as exc_info:
        ensure_gh_authenticated(cwd=Path('/repo'))

    assert exc_info.value.code == 'gh_not_authenticated'


def test_install_workflow_overwrites_target(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    class _FakeTemplate:
        def read_text(self, *, encoding: str) -> str:
            assert encoding == 'utf-8'
            return 'name: submit-prompt\\n'

    class _FakeFiles:
        def joinpath(self, name: str) -> _FakeTemplate:
            assert name == 'submit-prompt.yml'
            return _FakeTemplate()

    monkeypatch.setattr('dotpromptz.gitops.resolve_repo_root', lambda cwd=None: tmp_path)
    monkeypatch.setattr('dotpromptz.gitops.files', lambda package: _FakeFiles())

    target = install_workflow(cwd=tmp_path)

    assert target == tmp_path / '.github' / 'workflows' / 'submit-prompt.yml'
    assert target.read_text(encoding='utf-8') == 'name: submit-prompt\\n'


def test_install_workflow_skips_write_when_target_is_up_to_date(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    class _FakeTemplate:
        def read_text(self, *, encoding: str) -> str:
            assert encoding == 'utf-8'
            return 'name: submit-prompt\\n'

    class _FakeFiles:
        def joinpath(self, name: str) -> _FakeTemplate:
            assert name == 'submit-prompt.yml'
            return _FakeTemplate()

    target = tmp_path / '.github' / 'workflows' / 'submit-prompt.yml'
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text('name: submit-prompt\\n', encoding='utf-8')

    monkeypatch.setattr('dotpromptz.gitops.resolve_repo_root', lambda cwd=None: tmp_path)
    monkeypatch.setattr('dotpromptz.gitops.files', lambda package: _FakeFiles())

    def _unexpected_write_text(self: Path, data: str, *, encoding: str) -> int:
        raise AssertionError('Path.write_text should not be called when workflow is up to date')

    monkeypatch.setattr(Path, 'write_text', _unexpected_write_text)

    result = install_workflow(cwd=tmp_path)

    assert result == target
    assert target.read_text(encoding='utf-8') == 'name: submit-prompt\\n'


def test_submit_workflow_template_has_serialized_concurrency() -> None:
    content = files('dotpromptz.workflow').joinpath('submit-prompt.yml').read_text(encoding='utf-8')

    assert 'concurrency:' in content
    assert 'group: submit-prompt-${{ github.ref }}' in content
    assert 'cancel-in-progress: false' in content


def test_submit_workflow_template_contains_pr_fallback_flow() -> None:
    content = files('dotpromptz.workflow').joinpath('submit-prompt.yml').read_text(encoding='utf-8')

    assert 'pull-requests: write' in content
    assert 'Push fallback branch and create PR' in content
    assert '--head "$FALLBACK_BRANCH"' in content
    assert 'gh pr create' in content
    assert "steps.push.outputs.push_failed == 'true'" in content
    assert 'Fail workflow when direct push fails' in content
    assert 'fallback_pr_url' in content


def test_submit_prompt_runs_steps_in_order(monkeypatch: pytest.MonkeyPatch) -> None:
    calls: list[str] = []

    monkeypatch.setattr('dotpromptz.gitops.resolve_repo_root', lambda cwd=None: Path('/repo'))
    monkeypatch.setattr('dotpromptz.gitops._validate_prompt_path', lambda prompt_file, repo_root: 'foo/bar.prompt')
    monkeypatch.setattr(
        'dotpromptz.gitops.ensure_gh_authenticated',
        lambda cwd=None: calls.append('auth'),
    )
    monkeypatch.setattr(
        'dotpromptz.gitops.install_workflow',
        lambda cwd=None: calls.append('install') or Path('/repo/.github/workflows/submit-prompt.yml'),
    )
    monkeypatch.setattr(
        'dotpromptz.gitops.commit_and_push',
        lambda cwd=None: calls.append('push') or PushResult(committed=False),
    )
    monkeypatch.setattr('dotpromptz.gitops._resolve_current_branch', lambda repo_root: 'main')

    gh_command: list[str] = []

    def _fake_run_gh(args: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]:
        calls.append('run')
        gh_command.extend(args)
        return _cp()

    monkeypatch.setattr('dotpromptz.gitops._run_gh', _fake_run_gh)

    result = submit_prompt('foo/bar.prompt', send_mail=False, cwd=Path('/repo'))

    assert result == PushResult(committed=False, commit_sha=None, commit_message=None)
    assert calls == ['auth', 'install', 'push', 'run']
    assert 'send_mail=false' in gh_command
