"""
Tests for api/updates.py -- specifically the diagnostic code paths added
in fix/223-update-pull-failed-diagnostics (PR #227).

Tests cover the four new branches in _apply_update_inner():
  1. fetch fails  → network error message
  2. pull fails + diverged history  → recovery command with git reset --hard
  3. pull fails + no upstream tracking  → recovery command with set-upstream-to
  4. pull fails + generic fallback  → raw git output truncated at 300 chars
"""
from pathlib import Path
from unittest.mock import patch, call
import subprocess

import pytest


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_run_git_side_effect(*sequence):
    """Return a side_effect function that yields successive (stdout, ok) tuples."""
    it = iter(sequence)
    def _side_effect(args, cwd, timeout=10):
        return next(it)
    return _side_effect


# ---------------------------------------------------------------------------
# Path used for patching
# ---------------------------------------------------------------------------

_MODULE = 'api.updates'


# ---------------------------------------------------------------------------
# Tests for _apply_update_inner() diagnostic paths
# ---------------------------------------------------------------------------

class TestApplyUpdateDiagnostics:
    """New code paths introduced in PR #227."""

    def _apply(self, target, run_git_side_effect):
        """Call _apply_update_inner with _apply_lock bypassed and _run_git mocked."""
        from api import updates
        with patch(f'{_MODULE}._run_git', side_effect=run_git_side_effect), \
             patch.object(updates, '_apply_lock') as mock_lock:
            mock_lock.acquire.return_value = True
            mock_lock.release.return_value = None
            return updates._apply_update_inner(target)

    # ------------------------------------------------------------------
    # Path 1: fetch step fails → network error message
    # ------------------------------------------------------------------

    def test_fetch_failure_returns_network_error_message(self, tmp_path):
        """When git fetch fails, return a human-readable connection error."""
        (tmp_path / '.git').mkdir()

        from api import updates
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            # Call sequence: upstream query, fetch
            mock_run_git.side_effect = [
                ('origin/master', True),   # rev-parse @{upstream}
                ('', False),               # fetch fails
            ]
            result = updates._apply_update_inner('webui')

        assert result['ok'] is False
        msg = result['message'].lower()
        assert 'could not reach' in msg or 'internet connection' in msg or 'remote repository' in msg

    def test_fetch_failure_does_not_attempt_pull(self, tmp_path):
        """When fetch fails, pull is never called."""
        (tmp_path / '.git').mkdir()

        from api import updates
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            mock_run_git.side_effect = [
                ('origin/master', True),   # upstream query
                ('', False),               # fetch fails
            ]
            updates._apply_update_inner('webui')
            # Only 2 calls: upstream query + fetch. No pull call.
            assert mock_run_git.call_count == 2

    # ------------------------------------------------------------------
    # Path 2: pull fails + diverged history
    # ------------------------------------------------------------------

    def test_diverged_history_returns_reset_hard_command(self, tmp_path):
        """Diverged history produces a message with 'reset --hard'."""
        (tmp_path / '.git').mkdir()

        from api import updates
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            mock_run_git.side_effect = [
                ('origin/master', True),                          # upstream query
                ('', True),                                       # fetch succeeds
                ('', True),                                       # status --porcelain (clean)
                ('Not possible to fast-forward, aborting.', False),  # pull fails
            ]
            result = updates._apply_update_inner('webui')

        assert result['ok'] is False
        assert result.get('diverged') is True
        msg = result['message']
        assert 'reset --hard' in msg

    def test_diverged_history_message_contains_compare_ref(self, tmp_path):
        """Diverged history message includes the upstream ref."""
        (tmp_path / '.git').mkdir()

        from api import updates
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            mock_run_git.side_effect = [
                ('origin/feat/my-feature', True),   # upstream query
                ('', True),                         # fetch
                ('', True),                         # status (clean)
                ('Your branch and origin have diverged.', False),  # pull
            ]
            result = updates._apply_update_inner('webui')

        assert result['ok'] is False
        assert 'origin/feat/my-feature' in result['message']

    def test_diverged_matching_is_case_insensitive(self, tmp_path):
        """'DIVERGED' in uppercase is still detected."""
        (tmp_path / '.git').mkdir()

        from api import updates
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            mock_run_git.side_effect = [
                ('origin/master', True),
                ('', True),
                ('', True),
                ('DIVERGED from upstream', False),
            ]
            result = updates._apply_update_inner('webui')

        assert result['ok'] is False
        assert result.get('diverged') is True

    # ------------------------------------------------------------------
    # Path 3: pull fails + no upstream tracking configured
    # ------------------------------------------------------------------

    def test_no_tracking_returns_set_upstream_command(self, tmp_path):
        """Missing upstream tracking branch produces set-upstream-to message."""
        (tmp_path / '.git').mkdir()

        from api import updates
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            mock_run_git.side_effect = [
                ('origin/master', True),                               # upstream query
                ('', True),                                            # fetch
                ('', True),                                            # status (clean)
                ('There is no tracking information for the current branch.', False),  # pull
            ]
            result = updates._apply_update_inner('webui')

        assert result['ok'] is False
        assert 'set-upstream-to' in result['message']
        assert result.get('diverged') is None

    def test_no_tracking_alternate_phrasing(self, tmp_path):
        """'does not track' alternate git message is also detected."""
        (tmp_path / '.git').mkdir()

        from api import updates
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            mock_run_git.side_effect = [
                ('origin/master', True),
                ('', True),
                ('', True),
                ('fatal: The current branch local does not track a remote branch.', False),
            ]
            result = updates._apply_update_inner('webui')

        assert result['ok'] is False
        assert 'set-upstream-to' in result['message']

    def test_no_tracking_message_contains_compare_ref(self, tmp_path):
        """set-upstream-to message includes the upstream ref to configure."""
        (tmp_path / '.git').mkdir()

        from api import updates
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            mock_run_git.side_effect = [
                ('origin/main', True),
                ('', True),
                ('', True),
                ('no tracking information', False),
            ]
            result = updates._apply_update_inner('webui')

        assert result['ok'] is False
        assert 'origin/main' in result['message']

    # ------------------------------------------------------------------
    # Path 4: pull fails + generic fallback (truncated raw output)
    # ------------------------------------------------------------------

    def test_generic_failure_includes_truncated_git_output(self, tmp_path):
        """Generic pull failure includes up to 300 chars of git output."""
        (tmp_path / '.git').mkdir()
        long_error = 'X' * 500  # 500-char error from git

        from api import updates
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            mock_run_git.side_effect = [
                ('origin/master', True),
                ('', True),
                ('', True),
                (long_error, False),
            ]
            result = updates._apply_update_inner('webui')

        assert result['ok'] is False
        msg = result['message']
        # The raw output in the message must be truncated at 300 chars
        assert 'X' * 300 in msg
        assert 'X' * 301 not in msg

    def test_generic_failure_empty_output_shows_sentinel(self, tmp_path):
        """When git produces no output, message contains a fallback sentinel."""
        (tmp_path / '.git').mkdir()

        from api import updates
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            mock_run_git.side_effect = [
                ('origin/master', True),
                ('', True),
                ('', True),
                ('', False),   # pull fails with empty output
            ]
            result = updates._apply_update_inner('webui')

        assert result['ok'] is False
        assert 'no output' in result['message'].lower() or result['message']

    def test_generic_failure_does_not_set_diverged(self, tmp_path):
        """A generic pull failure must not set diverged=True."""
        (tmp_path / '.git').mkdir()

        from api import updates
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            mock_run_git.side_effect = [
                ('origin/master', True),
                ('', True),
                ('', True),
                ('Some unrecognized git error', False),
            ]
            result = updates._apply_update_inner('webui')

        assert result['ok'] is False
        assert not result.get('diverged')

    # ------------------------------------------------------------------
    # Regression: existing success path still works after fetch addition
    # ------------------------------------------------------------------

    def test_successful_update_still_returns_ok(self, tmp_path):
        """Fetch + status + pull success path returns ok=True (regression guard)."""
        (tmp_path / '.git').mkdir()

        from api import updates
        # Patch the cache's 'checked_at' key directly to avoid the lock
        # invalidation block raising. We use a fresh dict swap.
        fake_cache = {'webui': None, 'agent': None, 'checked_at': 1}
        with patch(f'{_MODULE}.REPO_ROOT', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git, \
             patch(f'{_MODULE}._update_cache', fake_cache), \
             patch(f'{_MODULE}._cache_lock'):
            mock_run_git.side_effect = [
                ('origin/master', True),          # upstream query
                ('', True),                       # fetch succeeds
                ('', True),                       # status (clean working tree)
                ('Already up to date.', True),    # pull succeeds
            ]
            result = updates._apply_update_inner('webui')

        assert result['ok'] is True

    # ------------------------------------------------------------------
    # Agent target works the same as webui target
    # ------------------------------------------------------------------

    def test_fetch_failure_for_agent_target(self, tmp_path):
        """Fetch failure path also works when target='agent'."""
        (tmp_path / '.git').mkdir()

        from api import updates
        with patch(f'{_MODULE}._AGENT_DIR', tmp_path), \
             patch(f'{_MODULE}._run_git') as mock_run_git:
            mock_run_git.side_effect = [
                ('origin/master', True),
                ('', False),   # fetch fails
            ]
            result = updates._apply_update_inner('agent')

        assert result['ok'] is False
        assert 'could not reach' in result['message'].lower() or \
               'internet' in result['message'].lower() or \
               'remote' in result['message'].lower()
