"""Tests for git_ops.py: push, unpushed detection, GitHub remote setup."""

import io
import os
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, call, patch

import pytest

import git_ops


# ═══════════════════════════════════════════════════════════════════════════
# has_unpushed_commits
# ═══════════════════════════════════════════════════════════════════════════


class TestHasUnpushedCommits:
    def test_returns_false_when_no_commits(self, tmp_path: Path):
        with patch("git_ops.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=128)
            assert git_ops.has_unpushed_commits(tmp_path) is False

    def test_returns_true_when_no_remote_branch(self, tmp_path: Path):
        with patch("git_ops.subprocess.run") as mock_run:
            head_ok = MagicMock(returncode=0)
            no_remote = MagicMock(returncode=128)
            mock_run.side_effect = [head_ok, no_remote]
            with patch("git_ops.get_current_branch", return_value="main"):
                assert git_ops.has_unpushed_commits(tmp_path) is True

    def test_returns_true_when_ahead(self, tmp_path: Path):
        with patch("git_ops.subprocess.run") as mock_run:
            mock_run.side_effect = [
                MagicMock(returncode=0),           # rev-parse HEAD
                MagicMock(returncode=0),           # rev-parse origin/main
                MagicMock(returncode=0),           # fetch
                MagicMock(returncode=0, stdout="3\n"),  # rev-list count
            ]
            with patch("git_ops.get_current_branch", return_value="main"):
                assert git_ops.has_unpushed_commits(tmp_path) is True

    def test_returns_false_when_not_ahead(self, tmp_path: Path):
        with patch("git_ops.subprocess.run") as mock_run:
            mock_run.side_effect = [
                MagicMock(returncode=0),
                MagicMock(returncode=0),
                MagicMock(returncode=0),
                MagicMock(returncode=0, stdout="0\n"),
            ]
            with patch("git_ops.get_current_branch", return_value="main"):
                assert git_ops.has_unpushed_commits(tmp_path) is False

    def test_returns_false_on_exception(self, tmp_path: Path):
        with patch("git_ops.subprocess.run", side_effect=OSError("fail")):
            assert git_ops.has_unpushed_commits(tmp_path) is False


# ═══════════════════════════════════════════════════════════════════════════
# push_commits
# ═══════════════════════════════════════════════════════════════════════════


class TestPushCommits:
    def test_success_first_push(self, tmp_path: Path):
        log_fh = io.StringIO()
        with patch("git_ops.subprocess.run") as mock_run, \
             patch("git_ops.get_current_branch", return_value="main"):
            mock_run.side_effect = [
                MagicMock(returncode=128),  # no origin/main → first push
                MagicMock(returncode=0),    # push succeeds
            ]
            assert git_ops.push_commits(tmp_path, log_fh) is True
        assert "succeeded" in log_fh.getvalue().lower()

    def test_success_existing_remote(self, tmp_path: Path):
        log_fh = io.StringIO()
        with patch("git_ops.subprocess.run") as mock_run, \
             patch("git_ops.get_current_branch", return_value="main"):
            mock_run.side_effect = [
                MagicMock(returncode=0),  # origin/main exists
                MagicMock(returncode=0),  # push succeeds
            ]
            assert git_ops.push_commits(tmp_path, log_fh) is True

    def test_non_fast_forward_rebase_retry(self, tmp_path: Path):
        log_fh = io.StringIO()
        with patch("git_ops.subprocess.run") as mock_run, \
             patch("git_ops.get_current_branch", return_value="main"):
            mock_run.side_effect = [
                MagicMock(returncode=0),  # origin/main exists
                MagicMock(returncode=1, stderr="non-fast-forward"),  # push rejected
                MagicMock(returncode=0),  # rebase OK
                MagicMock(returncode=0),  # push retry OK
            ]
            assert git_ops.push_commits(tmp_path, log_fh) is True
        assert "rebase" in log_fh.getvalue().lower()

    def test_non_fast_forward_rebase_fails(self, tmp_path: Path):
        log_fh = io.StringIO()
        with patch("git_ops.subprocess.run") as mock_run, \
             patch("git_ops.get_current_branch", return_value="main"):
            mock_run.side_effect = [
                MagicMock(returncode=0),
                MagicMock(returncode=1, stderr="rejected"),
                MagicMock(returncode=1, stderr="conflict"),
            ]
            assert git_ops.push_commits(tmp_path, log_fh) is False

    def test_generic_push_failure(self, tmp_path: Path):
        log_fh = io.StringIO()
        with patch("git_ops.subprocess.run") as mock_run, \
             patch("git_ops.get_current_branch", return_value="main"):
            mock_run.side_effect = [
                MagicMock(returncode=0),
                MagicMock(returncode=1, stderr="permission denied"),
            ]
            assert git_ops.push_commits(tmp_path, log_fh) is False

    def test_exception_during_push(self, tmp_path: Path):
        log_fh = io.StringIO()
        with patch("git_ops.subprocess.run", side_effect=Exception("timeout")), \
             patch("git_ops.get_current_branch", return_value="main"):
            assert git_ops.push_commits(tmp_path, log_fh) is False
        assert "error" in log_fh.getvalue().lower()


# ═══════════════════════════════════════════════════════════════════════════
# push_pending_commits
# ═══════════════════════════════════════════════════════════════════════════


class TestPushPendingCommits:
    def test_skips_when_nothing_to_push(self, tmp_path: Path):
        log_fh = io.StringIO()
        with patch("git_ops.has_unpushed_commits", return_value=False):
            assert git_ops.push_pending_commits(tmp_path, log_fh) is True
        assert "skipping" in log_fh.getvalue().lower()

    def test_calls_before_push_callback(self, tmp_path: Path):
        log_fh = io.StringIO()
        callback = MagicMock()
        with patch("git_ops.has_unpushed_commits", return_value=True), \
             patch("git_ops.push_commits", return_value=True):
            git_ops.push_pending_commits(tmp_path, log_fh, before_push=callback)
        callback.assert_called_once()

    def test_pushes_when_commits_exist(self, tmp_path: Path):
        log_fh = io.StringIO()
        with patch("git_ops.has_unpushed_commits", return_value=True), \
             patch("git_ops.push_commits", return_value=True) as mock_push:
            assert git_ops.push_pending_commits(tmp_path, log_fh) is True
        mock_push.assert_called_once()


# ═══════════════════════════════════════════════════════════════════════════
# ensure_github_remote
# ═══════════════════════════════════════════════════════════════════════════


class TestEnsureGithubRemote:
    def test_noop_when_remote_exists(self, tmp_path: Path):
        with patch("git_ops.subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=0, stdout="git@github.com:org/repo.git\n",
            )
            git_ops.ensure_github_remote(tmp_path, "my-app")
        assert mock_run.call_count == 1

    def test_creates_repo_when_no_remote(self, tmp_path: Path):
        with patch("git_ops.subprocess.run") as mock_run:
            mock_run.side_effect = [
                MagicMock(returncode=1, stdout=""),
                MagicMock(returncode=0, stdout="github.com/user/repo\n"),
            ]
            git_ops.ensure_github_remote(tmp_path, "my-app")
        assert mock_run.call_count == 2

    def test_handles_already_exists(self, tmp_path: Path):
        with patch("git_ops.subprocess.run") as mock_run:
            mock_run.side_effect = [
                MagicMock(returncode=1, stdout=""),
                MagicMock(returncode=1, stderr="already exists"),
                MagicMock(returncode=0, stdout="myuser\n"),
                MagicMock(returncode=0),
            ]
            git_ops.ensure_github_remote(tmp_path, "my-app")

    def test_handles_exception_gracefully(self, tmp_path: Path, capsys):
        with patch("git_ops.subprocess.run") as mock_run:
            mock_run.side_effect = [
                MagicMock(returncode=1, stdout=""),
                OSError("gh not installed"),
            ]
            git_ops.ensure_github_remote(tmp_path, "my-app")
        captured = capsys.readouterr()
        assert "warning" in captured.err.lower()
