import sys as _sys
from pathlib import Path as _Path
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
from mutation_profile_fixture import shared_mutation_env  # noqa: E402
"""Regression coverage for the remote/repo mismatch guard (#530).

Bare ``remote=prgs`` historically resolved to ``Scaled-Tech-Consulting/Timesheet``
(the hardcoded ``REMOTES`` default) instead of ``Scaled-Tech-Consulting/Gitea-Tools``,
which is what the local git remote actually points at. That silent mismatch caused
false 404s and risked mutating the wrong repository. The guard fails closed when the
MCP-resolved org/repo disagrees with the local git remote URL and the caller did not
pass explicit ``org``/``repo``.
"""

import os
import unittest
from unittest import mock

import remote_repo_guard


LOCAL_GITEA_TOOLS_URL = "https://gitea.prgs.cc/Scaled-Tech-Consulting/Gitea-Tools.git"


class TestAssessRemoteRepoMatch(unittest.TestCase):
    def test_explicit_org_and_repo_skips_guard(self):
        # Caller supplied both org and repo explicitly: never block, even on mismatch.
        assessment = remote_repo_guard.assess_remote_repo_match(
            remote="prgs",
            resolved_org="Scaled-Tech-Consulting",
            resolved_repo="Timesheet",
            local_remote_url=LOCAL_GITEA_TOOLS_URL,
            org_explicit=True,
            repo_explicit=True,
        )
        self.assertTrue(assessment["proven"])
        self.assertFalse(assessment["block"])
        self.assertEqual(assessment["reasons"], [])

    def test_missing_local_remote_url_skips_guard(self):
        # Best-effort lookup: no local remote URL (non-checkout usage) => do not block.
        assessment = remote_repo_guard.assess_remote_repo_match(
            remote="prgs",
            resolved_org="Scaled-Tech-Consulting",
            resolved_repo="Timesheet",
            local_remote_url=None,
            org_explicit=False,
            repo_explicit=False,
        )
        self.assertTrue(assessment["proven"])
        self.assertFalse(assessment["block"])

    def test_matching_repo_is_proven(self):
        assessment = remote_repo_guard.assess_remote_repo_match(
            remote="prgs",
            resolved_org="Scaled-Tech-Consulting",
            resolved_repo="Gitea-Tools",
            local_remote_url=LOCAL_GITEA_TOOLS_URL,
            org_explicit=False,
            repo_explicit=False,
        )
        self.assertTrue(assessment["proven"])
        self.assertFalse(assessment["block"])

    def test_regression_prgs_default_timesheet_vs_local_gitea_tools(self):
        # The exact #530 scenario: default repo Timesheet, local remote Gitea-Tools.
        assessment = remote_repo_guard.assess_remote_repo_match(
            remote="prgs",
            resolved_org="Scaled-Tech-Consulting",
            resolved_repo="Timesheet",
            local_remote_url=LOCAL_GITEA_TOOLS_URL,
            org_explicit=False,
            repo_explicit=False,
        )
        self.assertFalse(assessment["proven"])
        self.assertTrue(assessment["block"])
        self.assertTrue(assessment["reasons"])
        self.assertEqual(assessment["resolved_repo"], "Timesheet")
        self.assertEqual(assessment["resolved_org"], "Scaled-Tech-Consulting")

    def test_only_org_explicit_still_checks_repo(self):
        # Repo left as default => still guarded even if org was explicit.
        assessment = remote_repo_guard.assess_remote_repo_match(
            remote="prgs",
            resolved_org="Scaled-Tech-Consulting",
            resolved_repo="Timesheet",
            local_remote_url=LOCAL_GITEA_TOOLS_URL,
            org_explicit=True,
            repo_explicit=False,
        )
        self.assertTrue(assessment["block"])

    def test_case_insensitive_match(self):
        assessment = remote_repo_guard.assess_remote_repo_match(
            remote="prgs",
            resolved_org="scaled-tech-consulting",
            resolved_repo="gitea-tools",
            local_remote_url=LOCAL_GITEA_TOOLS_URL,
            org_explicit=False,
            repo_explicit=False,
        )
        self.assertTrue(assessment["proven"])

    def test_format_error_mentions_resolved_and_local(self):
        assessment = remote_repo_guard.assess_remote_repo_match(
            remote="prgs",
            resolved_org="Scaled-Tech-Consulting",
            resolved_repo="Timesheet",
            local_remote_url=LOCAL_GITEA_TOOLS_URL,
            org_explicit=False,
            repo_explicit=False,
        )
        message = remote_repo_guard.format_remote_repo_guard_error(assessment)
        self.assertIn("Timesheet", message)
        self.assertIn("Gitea-Tools", message)
        self.assertIn("org=", message)
        self.assertIn("repo=", message)
        # Recovery text must name a tool that actually accepts org/repo (#530 follow-up).
        self.assertIn("mcp_get_control_plane_guide", message)
        self.assertIn("schema does not list", message)

    def test_parse_org_repo_from_https_url(self):
        parsed = remote_repo_guard.parse_org_repo_from_remote_url(LOCAL_GITEA_TOOLS_URL)
        self.assertEqual(parsed, ("Scaled-Tech-Consulting", "Gitea-Tools"))

    def test_parse_org_repo_from_ssh_url(self):
        parsed = remote_repo_guard.parse_org_repo_from_remote_url(
            "git@gitea.prgs.cc:Scaled-Tech-Consulting/Gitea-Tools.git"
        )
        self.assertEqual(parsed, ("Scaled-Tech-Consulting", "Gitea-Tools"))

    def test_parse_org_repo_empty(self):
        self.assertIsNone(remote_repo_guard.parse_org_repo_from_remote_url(None))
        self.assertIsNone(remote_repo_guard.parse_org_repo_from_remote_url(""))


class TestResolveServerWiring(unittest.TestCase):
    """The guard is wired into gitea_mcp_server._resolve, so every read/lookup/
    mutation tool that resolves a target fails closed on a repo mismatch.

    Under pytest the guard is bypassed unless GITEA_FORCE_REMOTE_REPO_CHECK is set,
    so these tests set the force flag and patch the local remote lookup + REMOTES.
    """

    def setUp(self):
        import gitea_mcp_server

        self.server = gitea_mcp_server

        force = mock.patch.dict(
            os.environ, {"GITEA_FORCE_REMOTE_REPO_CHECK": "1"}, clear=False
        )
        force.start()
        self.addCleanup(force.stop)

        url_patch = mock.patch.object(
            gitea_mcp_server,
            "_local_git_remote_url",
            return_value=LOCAL_GITEA_TOOLS_URL,
        )
        url_patch.start()
        self.addCleanup(url_patch.stop)

    def _set_prgs_default_repo(self, repo):
        original = dict(self.server.REMOTES["prgs"])
        self.addCleanup(self.server.REMOTES.__setitem__, "prgs", original)
        self.server.REMOTES["prgs"] = {**original, "repo": repo}

    def test_resolve_prefers_workspace_over_timesheet_default(self):
        """#714: bare remote=prgs must use workspace Gitea-Tools, not REMOTES Timesheet."""
        self._set_prgs_default_repo("Timesheet")
        host, org, repo = self.server._resolve("prgs", None, None, None)
        self.assertEqual(org, "Scaled-Tech-Consulting")
        self.assertEqual(repo, "Gitea-Tools")

    def test_resolve_allows_explicit_org_and_repo(self):
        self._set_prgs_default_repo("Timesheet")
        # Explicit org/repo is authoritative even if it does not match local remote.
        host, org, repo = self.server._resolve(
            "prgs", None, "Scaled-Tech-Consulting", "Timesheet"
        )
        self.assertEqual((org, repo), ("Scaled-Tech-Consulting", "Timesheet"))

    def test_resolve_allows_matching_default(self):
        self._set_prgs_default_repo("Gitea-Tools")
        host, org, repo = self.server._resolve("prgs", None, None, None)
        self.assertEqual((org, repo), ("Scaled-Tech-Consulting", "Gitea-Tools"))

    def test_lookup_tool_cannot_silently_query_different_repo(self):
        # gitea_view_issue resolves the target via _resolve first, so a bare
        # remote=prgs pointing at the wrong default repo fails closed before any
        # API call is made.
        self._set_prgs_default_repo("Timesheet")
        with self.assertRaises(RuntimeError):
            self.server.gitea_view_issue(issue_number=1, remote="prgs")

    def test_control_plane_guide_accepts_explicit_gitea_tools(self):
        """Guide schema accepts org/repo and resolves Gitea-Tools, not Timesheet."""
        self._set_prgs_default_repo("Timesheet")
        with mock.patch.object(
            self.server, "api_request", return_value={"login": "guide-bot"}
        ), mock.patch.object(
            self.server, "get_auth_header", return_value="Basic dGVzdA=="
        ), mock.patch.dict(
            os.environ,
            {
                "GITEA_PROFILE_NAME": "author-test",
                "GITEA_ALLOWED_OPERATIONS": "gitea.read",
                "GITEA_FORBIDDEN_OPERATIONS": "gitea.pr.merge",
            },
            clear=False,
        ):
            guide = self.server.mcp_get_control_plane_guide(
                remote="prgs",
                org="Scaled-Tech-Consulting",
                repo="Gitea-Tools",
            )
        self.assertTrue(guide["read_only"])
        self.assertEqual(
            guide["resolved_repository"],
            {"org": "Scaled-Tech-Consulting", "repo": "Gitea-Tools"},
        )
        self.assertNotEqual(guide["resolved_repository"]["repo"], "Timesheet")

    def test_control_plane_guide_bare_remote_aligns_to_local_gitea_tools(self):
        """Bare remote=prgs in a Gitea-Tools worktree must not stay on Timesheet."""
        self._set_prgs_default_repo("Timesheet")
        with mock.patch.object(
            self.server, "api_request", return_value={"login": "guide-bot"}
        ), mock.patch.object(
            self.server, "get_auth_header", return_value="Basic dGVzdA=="
        ), mock.patch.dict(
            os.environ,
            {
                "GITEA_PROFILE_NAME": "author-test",
                "GITEA_ALLOWED_OPERATIONS": "gitea.read",
                "GITEA_FORBIDDEN_OPERATIONS": "gitea.pr.merge",
            },
            clear=False,
        ):
            guide = self.server.mcp_get_control_plane_guide(remote="prgs")
        self.assertEqual(guide["resolved_repository"]["repo"], "Gitea-Tools")
        self.assertEqual(
            guide["resolved_repository"]["org"], "Scaled-Tech-Consulting"
        )

    def test_control_plane_guide_wrong_default_fails_closed_without_local(self):
        """Without local remote context, mismatched defaults still fail closed."""
        self._set_prgs_default_repo("Timesheet")
        with mock.patch.object(
            self.server, "_local_git_remote_url", return_value=None
        ):
            # No local URL → preference cannot recover; bare resolve uses Timesheet
            # and guard is skipped only because local URL is missing (best-effort).
            host, org, repo = self.server._resolve_control_plane_guide_target(
                "prgs", None, None, None
            )
        self.assertEqual(repo, "Timesheet")

    def test_control_plane_guide_schema_recovery_matches_parameters(self):
        """Remediation must only recommend parameters the guide actually accepts."""
        import inspect

        sig = inspect.signature(self.server.mcp_get_control_plane_guide)
        self.assertIn("org", sig.parameters)
        self.assertIn("repo", sig.parameters)
        self.assertIn("remote", sig.parameters)
        remediation = remote_repo_guard.REMEDIATION
        self.assertIn("mcp_get_control_plane_guide", remediation)
        self.assertIn("org=", remediation)
        self.assertIn("repo=", remediation)


if __name__ == "__main__":
    unittest.main()
