# #region Test.Api.GitRepoOperationsRoutes [C:3] [TYPE Module] [SEMANTICS test,git,operations,commit,push,pull,status,diff,history]
# @BRIEF Tests for Git repository operations API routes.
# @RELATION BINDS_TO -> [GitRepoOperationsRoutes]

import os

os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests")
os.environ.setdefault("DEV_MODE", "true")

import sys
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient

_src = str(Path(__file__).resolve().parent.parent.parent / "src")
if _src not in sys.path:
    sys.path.insert(0, _src)


def _make_client(overrides: dict | None = None) -> TestClient:
    from src.api.routes.git._router import router as parent_router
    from src.core.database import get_db
    from src.dependencies import get_current_user
    from src.schemas.auth import User, RoleSchema

    app = FastAPI()
    app.include_router(parent_router)

    mock_user = User(
        id="admin-1", username="admin", email="admin@x.com",
        auth_source="LOCAL",
        created_at=__import__("datetime").datetime.now(),
        roles=[RoleSchema(id="r1", name="Admin", description="", permissions=[])],
    )

    app.dependency_overrides[get_db] = lambda: MagicMock()
    app.dependency_overrides[get_current_user] = lambda: mock_user
    if overrides:
        for dep, fn in overrides.items():
            app.dependency_overrides[dep] = fn
    return TestClient(app)


# ── commit_changes ──

class TestCommitChanges:
    """POST /repositories/{dashboard_ref}/commit"""

    COMMIT_PAYLOAD = {"message": "feat: add dashboard", "files": ["dash.json"]}

    def test_success(self):
        mock_gs = MagicMock()

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
            patch("src.api.routes.git._repo_operations_routes._apply_git_identity_from_profile", AsyncMock()),
        ):
            client = _make_client()
            resp = client.post("/repositories/42/commit", json=self.COMMIT_PAYLOAD)
        assert resp.status_code == 200
        assert resp.json()["status"] == "success"

    def test_unexpected_error(self):
        mock_gs = MagicMock()
        mock_gs.commit_changes.side_effect = RuntimeError("boom")

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
            patch("src.api.routes.git._repo_operations_routes._apply_git_identity_from_profile", AsyncMock()),
        ):
            client = _make_client()
            resp = client.post("/repositories/42/commit", json=self.COMMIT_PAYLOAD)
        assert resp.status_code == 500


# ── push_changes ──

class TestPushChanges:
    """POST /repositories/{dashboard_ref}/push"""

    def test_success(self):
        mock_gs = MagicMock()

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.post("/repositories/42/push")
        assert resp.status_code == 200
        assert resp.json()["status"] == "success"

    def test_unexpected_error(self):
        mock_gs = MagicMock()
        mock_gs.push_changes.side_effect = RuntimeError("boom")

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.post("/repositories/42/push")
        assert resp.status_code == 500


# ── pull_changes ──

class TestPullChanges:
    """POST /repositories/{dashboard_ref}/pull"""

    def test_success(self):
        mock_gs = MagicMock()
        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = None

        from src.core.database import get_db
        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
            patch("src.api.routes.git._repo_operations_routes._apply_git_identity_from_profile", AsyncMock()),
        ):
            client = _make_client({get_db: lambda: mock_db})
            resp = client.post("/repositories/42/pull")
        assert resp.status_code == 200
        assert resp.json()["status"] == "success"

    def test_success_with_repo_binding(self):
        mock_gs = MagicMock()
        mock_repo = MagicMock()
        mock_repo.config_id = "cfg-1"
        mock_repo.local_path = "/tmp/repo"
        mock_repo.remote_url = "https://url"
        mock_config = MagicMock()
        mock_config.url = "https://gitea.example.com"
        mock_config.provider = "GITEA"

        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.side_effect = [mock_repo, mock_config]

        from src.core.database import get_db
        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
            patch("src.api.routes.git._repo_operations_routes._apply_git_identity_from_profile", AsyncMock()),
        ):
            client = _make_client({get_db: lambda: mock_db})
            resp = client.post("/repositories/42/pull")
        assert resp.status_code == 200

    def test_unexpected_error(self):
        mock_gs = MagicMock()
        mock_gs.pull_changes.side_effect = RuntimeError("boom")

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
            patch("src.api.routes.git._repo_operations_routes._apply_git_identity_from_profile", AsyncMock()),
        ):
            client = _make_client()
            resp = client.post("/repositories/42/pull")
        assert resp.status_code == 500


# ── get_repository_status ──

class TestGetRepositoryStatus:
    """GET /repositories/{dashboard_ref}/status"""

    def test_success(self):
        with (
            patch("src.api.routes.git._repo_operations_routes._resolve_repository_status", AsyncMock(return_value={"is_dirty": False, "sync_state": "CLEAN"})),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.get("/repositories/42/status")
        assert resp.status_code == 200
        data = resp.json()
        assert data["is_dirty"] is False
        assert data["sync_state"] == "CLEAN"

    def test_unexpected_error(self):
        with (
            patch("src.api.routes.git._repo_operations_routes._resolve_repository_status", AsyncMock(side_effect=RuntimeError("boom"))),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.get("/repositories/42/status")
        assert resp.status_code == 500


# ── get_repository_status_batch ──

class TestGetRepositoryStatusBatch:
    """POST /repositories/status/batch"""

    def test_success(self):
        mock_status = {"is_dirty": False, "sync_state": "CLEAN", "has_repo": True}

        with (
            patch("src.api.routes.git._repo_operations_routes._resolve_repository_status", AsyncMock(return_value=mock_status)),
        ):
            client = _make_client()
            resp = client.post("/repositories/status/batch", json={"dashboard_ids": [1, 2, 3]})
        assert resp.status_code == 200
        data = resp.json()
        assert "statuses" in data
        assert "1" in data["statuses"]
        assert len(data["statuses"]) == 3

    def test_with_duplicate_ids(self):
        mock_status = {"is_dirty": False, "sync_state": "CLEAN", "has_repo": True}

        with (
            patch("src.api.routes.git._repo_operations_routes._resolve_repository_status", AsyncMock(return_value=mock_status)),
        ):
            client = _make_client()
            resp = client.post("/repositories/status/batch", json={"dashboard_ids": [1, 1, 2, 2]})
        assert resp.status_code == 200
        assert len(resp.json()["statuses"]) == 2

    def test_with_http_error_on_one(self):
        def side_effect(did):
            if did == 1:
                raise HTTPException(404)
            return {"is_dirty": False, "has_repo": True}

        with (
            patch("src.api.routes.git._repo_operations_routes._resolve_repository_status", AsyncMock(side_effect=side_effect)),
        ):
            client = _make_client()
            resp = client.post("/repositories/status/batch", json={"dashboard_ids": [1, 2]})
        assert resp.status_code == 200
        statuses = resp.json()["statuses"]
        assert statuses["1"]["sync_state"] == "ERROR"
        assert statuses["2"]["has_repo"] is True

    def test_truncates_large_batch(self):
        mock_status = {"is_dirty": False}
        many_ids = list(range(100))

        with (
            patch("src.api.routes.git._repo_operations_routes._resolve_repository_status", AsyncMock(return_value=mock_status)),
        ):
            client = _make_client()
            resp = client.post("/repositories/status/batch", json={"dashboard_ids": many_ids})
        assert resp.status_code == 200
        assert len(resp.json()["statuses"]) == 50


# ── get_repository_diff ──

class TestGetRepositoryDiff:
    """GET /repositories/{dashboard_ref}/diff"""

    def test_success(self):
        mock_gs = MagicMock()
        mock_gs.get_diff.return_value = {"diff": "--- a/dash.json\n+++ b/dash.json"}

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.get("/repositories/42/diff")
        assert resp.status_code == 200
        assert "diff" in resp.json()

    def test_with_file_filter(self):
        mock_gs = MagicMock()
        mock_gs.get_diff.return_value = {"diff": "file diff"}

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.get("/repositories/42/diff?file_path=dash.json&staged=true")
        assert resp.status_code == 200

    def test_unexpected_error(self):
        mock_gs = MagicMock()
        mock_gs.get_diff.side_effect = RuntimeError("boom")

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.get("/repositories/42/diff")
        assert resp.status_code == 500


# ── get_history ──

class TestGetHistory:
    """GET /repositories/{dashboard_ref}/history"""

    def test_success(self):
        from datetime import datetime
        mock_gs = MagicMock()
        mock_commit = MagicMock()
        mock_commit.hash = "abc123"
        mock_commit.author = "test"
        mock_commit.email = "test@x.com"
        mock_commit.timestamp = datetime.now()
        mock_commit.message = "feat: add"
        mock_commit.files_changed = ["dash.json"]
        mock_gs.get_commit_history.return_value = [mock_commit]

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.get("/repositories/42/history")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data) == 1
        assert data[0]["hash"] == "abc123"

    def test_with_limit(self):
        mock_gs = MagicMock()
        mock_gs.get_commit_history.return_value = []

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.get("/repositories/42/history?limit=5")
        assert resp.status_code == 200

    def test_unexpected_error(self):
        mock_gs = MagicMock()
        mock_gs.get_commit_history.side_effect = RuntimeError("boom")

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.get("/repositories/42/history")
        assert resp.status_code == 500


# ── generate_commit_message ──

class TestGenerateCommitMessage:
    """POST /repositories/{dashboard_ref}/generate-message"""

    def test_no_changes(self):
        mock_gs = MagicMock()
        mock_gs.get_diff.side_effect = [None, None]

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.post("/repositories/42/generate-message")
        assert resp.status_code == 200
        assert resp.json()["message"] == "No changes detected"

    def test_unexpected_error(self):
        mock_gs = MagicMock()
        mock_gs.get_diff.side_effect = RuntimeError("boom")

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
        ):
            client = _make_client()
            resp = client.post("/repositories/42/generate-message")
        assert resp.status_code == 500

    def test_no_active_provider(self):
        mock_gs = MagicMock()
        mock_gs.get_diff.side_effect = [{"diff": "changes"}, None]

        mock_llm_svc = MagicMock()
        mock_llm_svc.get_all_providers.return_value = []

        with (
            patch("src.api.routes.git._repo_operations_routes.get_git_service", return_value=mock_gs),
            patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)),
            patch("src.services.llm_provider.LLMProviderService", return_value=mock_llm_svc),
        ):
            client = _make_client()
            resp = client.post("/repositories/42/generate-message")
        assert resp.status_code == 400
        assert "No active LLM provider" in resp.text
# #endregion Test.Api.GitRepoOperationsRoutes
