"""Unit tests for migrate_profiles.py migration helper."""

import os
import sys
import json
import unittest
import tempfile
import shutil
from unittest.mock import patch
from io import StringIO

from _pytest.monkeypatch import MonkeyPatch

# Add project root to sys.path
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if PROJECT_ROOT not in sys.path:
    sys.path.insert(0, PROJECT_ROOT)

import migrate_profiles


class TestMigrateProfiles(unittest.TestCase):

    def setUp(self):
        self.temp_dir = tempfile.mkdtemp()
        self.v1_content = {
            "version": 1,
            "profiles": {
                "prgs": {
                    "base_url": "redacted-prgs-service",
                    "username": "jcwalker3",
                    "auth": {
                        "type": "keychain",
                        "id": "redacted-author-ref"
                    },
                    "default_owner": "Scaled-Tech-Consulting",
                    "execution_profile": "personal-prgs",
                    "allowed_operations": ["read", "comment"],
                    "forbidden_operations": ["approve", "merge"]
                },
                "mdcps": {
                    "base_url": "redacted-mdcps-service",
                    "username": "913443",
                    "auth": {
                        "type": "keychain",
                        "id": "redacted-mdcps-ref"
                    },
                    "default_owner": "Contractor",
                    "execution_profile": "mdcps",
                    "allowed_operations": ["read"],
                    "forbidden_operations": ["merge"]
                },
                "prgs-reviewer": {
                    "base_url": "redacted-prgs-service",
                    "username": "sysadmin",
                    "auth": {
                        "type": "keychain",
                        "id": "redacted-reviewer-ref"
                    },
                    "default_owner": "Scaled-Tech-Consulting",
                    "execution_profile": "prgs-reviewer",
                    "allowed_operations": [
                        "read", "review", "comment", "approve",
                        "request_changes", "merge"
                    ],
                    "forbidden_operations": ["branch", "commit", "push", "open_pr"]
                }
            }
        }
        self.input_file = os.path.join(self.temp_dir, "profiles.json")
        with open(self.input_file, "w") as f:
            json.dump(self.v1_content, f)

    def tearDown(self):
        shutil.rmtree(self.temp_dir)

    def test_migration_logic(self):
        """Test the structural transformation and capability mapping."""
        v2_data = migrate_profiles.migrate_v1_to_v2(self.v1_content)
        self.assertEqual(v2_data["version"], 2)

        # Check environment structure
        envs = v2_data["environments"]
        self.assertIn("prgs", envs)
        self.assertIn("mdcps", envs)

        # Check service and identity structure
        prgs_gitea = envs["prgs"]["services"]["gitea"]
        self.assertEqual(prgs_gitea["base_url"], "redacted-prgs-service")
        self.assertEqual(prgs_gitea["default_owner"], "Scaled-Tech-Consulting")

        author = prgs_gitea["identities"]["author"]
        self.assertEqual(author["username"], "jcwalker3")
        self.assertEqual(author["auth"]["id"], "redacted-author-ref")
        self.assertEqual(
            author["allowed_operations"], ["gitea.read", "gitea.pr.comment"]
        )
        self.assertEqual(
            author["forbidden_operations"],
            ["gitea.pr.approve", "gitea.pr.merge"],
        )

        reviewer = prgs_gitea["identities"]["reviewer"]
        self.assertEqual(reviewer["role"], "reviewer")
        self.assertEqual(reviewer["username"], "sysadmin")
        self.assertEqual(reviewer["auth"]["id"], "redacted-reviewer-ref")
        self.assertIn("gitea.pr.merge", reviewer["allowed_operations"])

    def test_alias_generation(self):
        """Test that aliases are correctly generated to support old profile names."""
        v2_data = migrate_profiles.migrate_v1_to_v2(self.v1_content)
        aliases = v2_data["aliases"]

        self.assertEqual(aliases["prgs"], "prgs.gitea.author")
        self.assertEqual(aliases["prgs-author"], "prgs.gitea.author")
        self.assertEqual(aliases["prgs-reviewer"], "prgs.gitea.reviewer")
        self.assertEqual(aliases["mdcps"], "mdcps.gitea.author")

    def test_no_secret_behavior(self):
        """Ensure secrets are never extracted, printed, or processed."""
        v2_data = migrate_profiles.migrate_v1_to_v2(self.v1_content)
        # Check that auth structures only contain keychain references, not credentials
        for env in v2_data["environments"].values():
            for svc in env["services"].values():
                for ident in svc["identities"].values():
                    auth = ident["auth"]
                    self.assertEqual(auth["type"], "keychain")
                    self.assertIn("id", auth)
                    self.assertNotIn("token", auth)
                    self.assertNotIn("password", auth)

    def test_validation(self):
        """Test that the generated v2 configuration validates against Gitea-Tools v2 loader."""
        v2_data = migrate_profiles.migrate_v1_to_v2(self.v1_content)
        self.assertTrue(migrate_profiles.validate_v2_data(v2_data))

    def test_dry_run_default(self):
        """Verify that running without -w prints generated config without modifying files."""
        monkeypatch = MonkeyPatch()
        mock_stdout = StringIO()
        monkeypatch.setattr(sys, "stdout", mock_stdout)
        try:
            output_file = os.path.join(self.temp_dir, "migrated_dry.json")
            test_args = [
                "migrate_profiles.py",
                "-i", self.input_file,
                "-o", output_file
            ]
            with patch.object(sys, "argv", test_args):
                with self.assertRaises(SystemExit) as cm:
                    migrate_profiles.main()
                self.assertEqual(cm.exception.code, 0)

            self.assertFalse(os.path.exists(output_file))
            self.assertFalse(os.path.exists(f"{self.input_file}.bak"))

            stdout_output = mock_stdout.getvalue()
        finally:
            monkeypatch.undo()
        self.assertIn("DRY-RUN MODE", stdout_output)
        self.assertIn("version", stdout_output)
        self.assertIn("identities", stdout_output)
        self.assertIn("aliases", stdout_output)
        self.assertNotIn("redacted-prgs-service", stdout_output)
        self.assertNotIn("redacted-mdcps-service", stdout_output)
        self.assertNotIn("redacted-author-ref", stdout_output)
        self.assertNotIn("redacted-mdcps-ref", stdout_output)
        self.assertNotIn("redacted-reviewer-ref", stdout_output)
        self.assertNotIn("keychain", stdout_output)
        self.assertNotIn("auth", stdout_output)

    def test_dry_run_hides_token_like_values(self):
        """Verify dry-run summary does not expose token-like auth metadata."""
        sensitive = json.loads(json.dumps(self.v1_content))
        sensitive["profiles"]["prgs"]["auth"]["id"] = "super-secret-token-value"
        with open(self.input_file, "w") as f:
            json.dump(sensitive, f)

        test_args = ["migrate_profiles.py", "-i", self.input_file]
        monkeypatch = MonkeyPatch()
        mock_stdout = StringIO()
        monkeypatch.setattr(sys, "stdout", mock_stdout)
        try:
            with patch.object(sys, "argv", test_args):
                with self.assertRaises(SystemExit) as cm:
                    migrate_profiles.main()
                self.assertEqual(cm.exception.code, 0)

            stdout_output = mock_stdout.getvalue()
        finally:
            monkeypatch.undo()
        self.assertNotIn("super-secret-token-value", stdout_output)
        self.assertNotIn("token", stdout_output.lower())

    def test_explicit_operations_are_preserved(self):
        """Explicit v1 permissions are canonicalized, not replaced by role defaults."""
        v1_data = json.loads(json.dumps(self.v1_content))
        v1_data["profiles"]["prgs-reviewer"]["allowed_operations"] = ["read"]
        v1_data["profiles"]["prgs-reviewer"]["forbidden_operations"] = ["merge"]

        v2_data = migrate_profiles.migrate_v1_to_v2(v1_data)
        reviewer = (
            v2_data["environments"]["prgs"]["services"]["gitea"]
            ["identities"]["reviewer"]
        )
        self.assertEqual(reviewer["allowed_operations"], ["gitea.read"])
        self.assertEqual(reviewer["forbidden_operations"], ["gitea.pr.merge"])

    def test_inferred_role_defaults_only_when_unambiguous(self):
        """Role defaults are allowed only for clear author/reviewer profiles."""
        v1_data = {
            "version": 1,
            "profiles": {
                "prgs-author": {
                    "base_url": "redacted-prgs-service",
                    "username": "jcwalker3",
                    "auth": {"type": "keychain", "id": "hidden-author-ref"},
                    "execution_profile": "prgs-author",
                }
            }
        }

        v2_data = migrate_profiles.migrate_v1_to_v2(v1_data)
        author = (
            v2_data["environments"]["prgs"]["services"]["gitea"]
            ["identities"]["author"]
        )
        self.assertEqual(
            author["allowed_operations"],
            migrate_profiles.AUTHOR_DEFAULT_ALLOWED,
        )
        self.assertEqual(
            author["forbidden_operations"],
            migrate_profiles.AUTHOR_DEFAULT_FORBIDDEN,
        )

    def test_ambiguous_permission_source_fails_closed(self):
        """A profile without explicit permissions or clear role must not widen."""
        v1_data = {
            "version": 1,
            "profiles": {
                "prgs": {
                    "base_url": "redacted-prgs-service",
                    "username": "jcwalker3",
                    "auth": {"type": "keychain", "id": "hidden-author-ref"},
                    "execution_profile": "personal-prgs",
                }
            }
        }

        with self.assertRaisesRegex(ValueError, "fail closed"):
            migrate_profiles.migrate_v1_to_v2(v1_data)

    def test_partial_permission_source_fails_closed(self):
        """Allowed without forbidden, or vice versa, is ambiguous."""
        v1_data = json.loads(json.dumps(self.v1_content))
        del v1_data["profiles"]["prgs"]["forbidden_operations"]

        with self.assertRaisesRegex(ValueError, "fail closed"):
            migrate_profiles.migrate_v1_to_v2(v1_data)

    def test_write_mode_and_backup(self):
        """Verify that write mode creates a backup and correctly saves the validated config."""
        output_file = os.path.join(self.temp_dir, "migrated.json")
        backup_file = os.path.join(self.temp_dir, "profiles_backup.json.bak")

        test_args = [
            "migrate_profiles.py",
            "-i", self.input_file,
            "-o", output_file,
            "--backup", backup_file,
            "-w"
        ]
        with patch.object(sys, "argv", test_args):
            migrate_profiles.main()

        # Verify backup exists and matches original v1 config
        self.assertTrue(os.path.exists(backup_file))
        with open(backup_file, "r") as f:
            backup_data = json.load(f)
        self.assertEqual(backup_data["version"], 1)
        self.assertIn("prgs", backup_data["profiles"])

        # Verify migrated v2 config exists and validates
        self.assertTrue(os.path.exists(output_file))
        with open(output_file, "r") as f:
            v2_data = json.load(f)
        self.assertEqual(v2_data["version"], 2)
        self.assertIn("environments", v2_data)
        self.assertEqual(v2_data["aliases"]["prgs"], "prgs.gitea.author")

    def test_malformed_input_fails_safely(self):
        """Test that malformed JSON or invalid version numbers cause a clean exit with code 1."""
        bad_json_file = os.path.join(self.temp_dir, "bad.json")
        with open(bad_json_file, "w") as f:
            f.write("{invalid-json}")

        test_args = ["migrate_profiles.py", "-i", bad_json_file]
        with patch.object(sys, "argv", test_args):
            with self.assertRaises(SystemExit) as cm:
                migrate_profiles.main()
            self.assertEqual(cm.exception.code, 1)

        bad_version_file = os.path.join(self.temp_dir, "bad_version.json")
        with open(bad_version_file, "w") as f:
            json.dump({"version": 3, "profiles": {}}, f)

        test_args = ["migrate_profiles.py", "-i", bad_version_file]
        with patch.object(sys, "argv", test_args):
            with self.assertRaises(SystemExit) as cm:
                migrate_profiles.main()
            self.assertEqual(cm.exception.code, 1)

    def test_reconciler_profile_migration(self):
        """Legacy reconciler shorthands migrate to valid canonical operations."""
        import gitea_config
        import reconciler_profile

        v1_data = {
            "version": 1,
            "profiles": {
                "prgs-reconciler": {
                    "base_url": "redacted-prgs-service",
                    "username": "reconciler-agent",
                    "auth": {"type": "keychain", "id": "reconciler-ref"},
                    "execution_profile": "prgs-reconciler",
                    "allowed_operations": [
                        "read",
                        "pr.close",
                        "pr.comment",
                        "issue.comment",
                        "issue.close",
                        "gitea.branch.delete",
                    ],
                    "forbidden_operations": [
                        "merge",
                        "approve",
                        "review",
                        "pr.create",
                        "branch.push",
                        "commit",
                    ],
                }
            }
        }
        v2_data = migrate_profiles.migrate_v1_to_v2(v1_data)
        reconciler = (
            v2_data["environments"]["prgs"]["services"]["gitea"]
            ["identities"]["reconciler"]
        )
        self.assertEqual(reconciler["role"], "reconciler")
        allowed = reconciler["allowed_operations"]
        forbidden = reconciler["forbidden_operations"]
        # No invalid shorthand remains
        for bad in ("pr.close", "pr.comment", "issue.close", "read", "merge"):
            self.assertNotIn(bad, allowed)
            self.assertNotIn(bad, forbidden)
        for required in (
            "gitea.read",
            "gitea.pr.close",
            "gitea.pr.comment",
            "gitea.issue.comment",
            "gitea.issue.close",
            "gitea.branch.delete",
        ):
            self.assertIn(required, allowed)
            # Production loader accepts every allowed op
            self.assertEqual(
                gitea_config.normalize_operation(required), required
            )
        self.assertEqual(v2_data["aliases"]["prgs-reconciler"], "prgs.gitea.reconciler")
        assessment = reconciler_profile.assess_reconciler_profile(allowed, forbidden)
        self.assertTrue(assessment["valid"])
        self.assertTrue(migrate_profiles.validate_v2_data(v2_data))

    def test_reconciler_profile_defaults(self):
        """Reconciler defaults are fully canonical and loader-valid."""
        import gitea_config
        import reconciler_profile

        v1_data = {
            "version": 1,
            "profiles": {
                "prgs-reconciler": {
                    "base_url": "redacted-prgs-service",
                    "username": "reconciler-agent",
                    "auth": {"type": "keychain", "id": "reconciler-ref"},
                    "execution_profile": "prgs-reconciler",
                }
            }
        }
        v2_data = migrate_profiles.migrate_v1_to_v2(v1_data)
        reconciler = (
            v2_data["environments"]["prgs"]["services"]["gitea"]
            ["identities"]["reconciler"]
        )
        self.assertEqual(reconciler["role"], "reconciler")
        self.assertEqual(
            reconciler["allowed_operations"],
            migrate_profiles.RECONCILER_DEFAULT_ALLOWED,
        )
        self.assertEqual(
            reconciler["forbidden_operations"],
            migrate_profiles.RECONCILER_DEFAULT_FORBIDDEN,
        )
        for op in reconciler["allowed_operations"]:
            self.assertEqual(gitea_config.normalize_operation(op), op)
            self.assertTrue(op.startswith("gitea."))
        assessment = reconciler_profile.assess_reconciler_profile(
            reconciler["allowed_operations"],
            reconciler["forbidden_operations"],
        )
        self.assertTrue(assessment["valid"])
        self.assertNotIn(
            "gitea.branch.delete", assessment["missing_recommended_operations"]
        )

    def test_reconciler_migration_idempotent_canonicalize(self):
        """Second canonicalize of already-canonical ops is a no-op."""
        first = migrate_profiles.canonicalize_operations(
            list(migrate_profiles.RECONCILER_DEFAULT_ALLOWED)
        )
        second = migrate_profiles.canonicalize_operations(first)
        self.assertEqual(first, second)
        self.assertEqual(first, list(migrate_profiles.RECONCILER_DEFAULT_ALLOWED))

    def test_reconciler_missing_required_fails_visibly(self):
        """Missing gitea.pr.close after migration fails closed (not silent drop)."""
        v1_data = {
            "version": 1,
            "profiles": {
                "prgs-reconciler": {
                    "base_url": "redacted-prgs-service",
                    "username": "reconciler-agent",
                    "auth": {"type": "keychain", "id": "reconciler-ref"},
                    "execution_profile": "prgs-reconciler",
                    "allowed_operations": ["read", "gitea.branch.delete"],
                    "forbidden_operations": ["merge"],
                }
            },
        }
        with self.assertRaisesRegex(ValueError, "missing required"):
            migrate_profiles.migrate_v1_to_v2(v1_data)

    def test_unknown_operation_fails_visibly(self):
        v1_data = {
            "version": 1,
            "profiles": {
                "prgs-author": {
                    "base_url": "redacted-prgs-service",
                    "username": "jcwalker3",
                    "auth": {"type": "keychain", "id": "hidden-author-ref"},
                    "execution_profile": "prgs-author",
                    "allowed_operations": ["read", "not.a.real.op"],
                    "forbidden_operations": ["merge"],
                }
            },
        }
        with self.assertRaisesRegex(ValueError, "cannot be canonicalized"):
            migrate_profiles.migrate_v1_to_v2(v1_data)

    def test_role_inference_author_reviewer_merger_reconciler(self):
        self.assertEqual(
            migrate_profiles.infer_role("prgs-author", "prgs-author"), "author"
        )
        self.assertEqual(
            migrate_profiles.infer_role("prgs-reviewer", "prgs-reviewer"),
            "reviewer",
        )
        self.assertEqual(
            migrate_profiles.infer_role("prgs-reconciler", "prgs-reconciler"),
            "reconciler",
        )
        self.assertIsNone(
            migrate_profiles.infer_role("prgs-merger", "prgs-merger")
        )


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

