"""
Tests for the AsyncBranchManager, focusing on the personal branch fix.

Unit tests (no credentials needed):
    pytest tests/test_branch_manager.py -v -k "unit"

Integration tests (requires env vars):
    export LOOKER_BASE_URL="https://your-instance.looker.com"
    export LOOKER_CLIENT_ID="your-client-id"
    export LOOKER_CLIENT_SECRET="your-client-secret"
    export LOOKER_PROJECT="your-project-name"
    pytest tests/test_branch_manager.py -v -k "integration"
"""

import asyncio
import os
import unittest
from unittest.mock import AsyncMock, MagicMock, patch, call

from looker_validator.branch_manager import AsyncBranchManager, ProjectState
from looker_validator.async_client import AsyncLookerClient


# ---------------------------------------------------------------------------
# Unit tests — mock the Looker API, no credentials needed
# ---------------------------------------------------------------------------

class TestUnitPersonalBranchRefFormat(unittest.TestCase):
    """Verify that setup_branch passes the bare branch name (not origin/...)
    to _checkout_personal_branch."""

    def test_unit_no_origin_prefix_in_personal_branch_ref(self):
        """The ref passed to _checkout_personal_branch must NOT have 'origin/' prefix."""
        client = AsyncMock(spec=AsyncLookerClient)
        manager = AsyncBranchManager(
            client=client,
            project="test_project",
            use_personal_branch=True,
        )

        branch_name = "dev-svc-looker-ad-service-account-zhcw"

        # Mock _checkout_personal_branch so we can inspect the argument
        manager._checkout_personal_branch = AsyncMock(return_value="dev-personal-branch")

        async def run():
            # Simulate that we're already in dev mode
            manager.branch = None
            manager.commit = None
            # Call setup_branch with the PR branch name
            await manager.setup_branch(branch=branch_name)

        asyncio.run(run())

        # Assert _checkout_personal_branch was called with the bare branch name
        manager._checkout_personal_branch.assert_called_once_with(branch_name)

        # Verify no "origin/" prefix
        actual_ref = manager._checkout_personal_branch.call_args[0][0]
        self.assertFalse(
            actual_ref.startswith("origin/"),
            f"Expected bare branch name, got '{actual_ref}' with 'origin/' prefix"
        )


class TestUnitPersonalBranchCheckoutFlow(unittest.TestCase):
    """Verify the full _checkout_personal_branch flow calls the right API methods."""

    def test_unit_checkout_personal_branch_api_calls(self):
        """_checkout_personal_branch should: switch to dev, find personal branch,
        checkout it, reset to remote, then hard_reset to the target ref."""
        client = AsyncMock(spec=AsyncLookerClient)

        # Mock API responses
        client.update_workspace = AsyncMock()
        client.get_all_branches = AsyncMock(return_value=[
            {"name": "dev-ci-user-abcd", "personal": True, "readonly": False},
            {"name": "main", "personal": False, "readonly": False, "ref": "aaa111"},
            {"name": "feature/MAG-1206-sst-campaigns", "personal": False, "readonly": False, "ref": "abc123def456"},
        ])
        client.checkout_branch = AsyncMock()
        client.reset_to_remote = AsyncMock()
        client.hard_reset_branch = AsyncMock()

        manager = AsyncBranchManager(
            client=client,
            project="test_project",
            use_personal_branch=True,
        )

        target_ref = "feature/MAG-1206-sst-campaigns"

        async def run():
            result = await manager._checkout_personal_branch(target_ref)
            return result

        result = asyncio.run(run())

        # Should have switched to dev mode
        client.update_workspace.assert_called_with("dev")

        # Should have checked out the CI user's personal branch
        client.checkout_branch.assert_called_with("test_project", "dev-ci-user-abcd")

        # Should have reset to remote
        client.reset_to_remote.assert_called_with("test_project")

        # Should have resolved branch name to commit SHA and used that
        client.hard_reset_branch.assert_called_with(
            "test_project", "dev-ci-user-abcd", "abc123def456"
        )
        self.assertEqual(result, "dev-ci-user-abcd")


class TestUnitSetupBranchModes(unittest.TestCase):
    """Verify setup_branch routes correctly for all three modes."""

    def _make_manager(self, use_personal_branch=False):
        client = AsyncMock(spec=AsyncLookerClient)
        client.update_workspace = AsyncMock()
        client.checkout_branch = AsyncMock()
        client.reset_to_remote = AsyncMock()
        client.get_active_branch_name = AsyncMock(return_value="some-branch")
        return AsyncBranchManager(
            client=client,
            project="test_project",
            use_personal_branch=use_personal_branch,
        ), client

    def test_unit_production_mode_when_no_branch_or_commit(self):
        """No branch or commit_ref should switch to production."""
        manager, client = self._make_manager()

        asyncio.run(manager.setup_branch(branch=None, commit_ref=None))
        client.update_workspace.assert_called_with("production")

    def test_unit_regular_branch_checkout_without_personal(self):
        """With use_personal_branch=False, should directly checkout the branch."""
        manager, client = self._make_manager(use_personal_branch=False)
        client.get_active_branch_name = AsyncMock(return_value="feature-branch")

        asyncio.run(manager.setup_branch(branch="feature-branch"))
        client.update_workspace.assert_called_with("dev")
        client.checkout_branch.assert_called_with("test_project", "feature-branch")

    def test_unit_personal_branch_mode_calls_checkout_personal(self):
        """With use_personal_branch=True, should call _checkout_personal_branch."""
        manager, client = self._make_manager(use_personal_branch=True)
        manager._checkout_personal_branch = AsyncMock(return_value="dev-ci-user")
        manager.verify_branch_checkout = AsyncMock(return_value=True)

        asyncio.run(manager.setup_branch(branch="my-feature"))

        # Must be called with bare branch name
        manager._checkout_personal_branch.assert_called_once_with("my-feature")


class TestUnitCleanup(unittest.TestCase):
    """Verify cleanup restores initial state."""

    def test_unit_restore_production_state(self):
        """If initial state was production, should restore to production."""
        client = AsyncMock(spec=AsyncLookerClient)
        client.update_workspace = AsyncMock()

        manager = AsyncBranchManager(client=client, project="test_project")
        manager.history = [
            ProjectState(project="test_project", workspace="production", branch="main", commit="abc123")
        ]

        asyncio.run(manager._restore_initial_state())
        client.update_workspace.assert_called_with("production")

    def test_unit_restore_dev_state(self):
        """If initial state was dev, should restore to dev and checkout original branch."""
        client = AsyncMock(spec=AsyncLookerClient)
        client.update_workspace = AsyncMock()
        client.checkout_branch = AsyncMock()

        manager = AsyncBranchManager(client=client, project="test_project")
        manager.history = [
            ProjectState(project="test_project", workspace="dev", branch="original-branch", commit="abc123")
        ]

        asyncio.run(manager._restore_initial_state())
        client.update_workspace.assert_called_with("dev")
        client.checkout_branch.assert_called_with("test_project", "original-branch")


class TestUnitCIBranchCheckoutFlow(unittest.TestCase):
    """Verify the CI branch checkout flow."""

    def _make_manager(self, ci_run_id="12345678"):
        client = AsyncMock(spec=AsyncLookerClient)
        client.update_workspace = AsyncMock()
        client.get_all_branches = AsyncMock(return_value=[
            {"name": "main", "personal": False, "readonly": False, "ref": "aaa111"},
            {"name": "feature/my-pr", "personal": False, "readonly": False, "ref": "abc123def456"},
        ])
        client.create_branch = AsyncMock()
        client.checkout_branch = AsyncMock()
        client.delete_branch = AsyncMock()
        client.get_active_branch_name = AsyncMock(return_value=f"ci-validation-{ci_run_id}")
        return AsyncBranchManager(
            client=client,
            project="test_project",
            use_ci_branch=True,
            ci_run_id=ci_run_id,
        ), client

    def test_unit_ci_branch_generates_correct_name(self):
        """_checkout_ci_branch should generate branch name ci-validation-{ci_run_id}."""
        manager, client = self._make_manager(ci_run_id="99887766")

        async def run():
            return await manager._checkout_ci_branch("feature/my-pr")

        result = asyncio.run(run())
        self.assertEqual(result, "ci-validation-99887766")

    def test_unit_ci_branch_creates_branch_with_resolved_sha(self):
        """_checkout_ci_branch should resolve ref to SHA and create branch from it."""
        manager, client = self._make_manager()

        async def run():
            return await manager._checkout_ci_branch("feature/my-pr")

        asyncio.run(run())

        # Should create branch with the resolved SHA
        client.create_branch.assert_called_once_with(
            "test_project", "ci-validation-12345678", "abc123def456"
        )

    def test_unit_ci_branch_sets_is_temp_branch(self):
        """_checkout_ci_branch should set is_temp_branch to True."""
        manager, client = self._make_manager()

        async def run():
            await manager._checkout_ci_branch("feature/my-pr")

        asyncio.run(run())
        self.assertTrue(manager.is_temp_branch)

    def test_unit_ci_branch_cleanup_deletes_branch(self):
        """Cleanup should delete the CI branch via existing temp branch cleanup."""
        manager, client = self._make_manager()
        # Simulate state after _checkout_ci_branch
        manager.is_temp_branch = True
        manager.branch = "ci-validation-12345678"
        manager.history = [
            ProjectState(project="test_project", workspace="dev", branch="main", commit="aaa111"),
            ProjectState(project="test_project", workspace="dev", branch="main", commit="aaa111"),
        ]

        async def run():
            await manager._cleanup_temp_branch()

        asyncio.run(run())
        client.delete_branch.assert_called_with("test_project", "ci-validation-12345678")
        self.assertFalse(manager.is_temp_branch)

    def test_unit_setup_branch_routes_to_ci_branch(self):
        """setup_branch should route to _checkout_ci_branch when use_ci_branch=True."""
        manager, client = self._make_manager()
        manager._checkout_ci_branch = AsyncMock(return_value="ci-validation-12345678")
        manager.verify_branch_checkout = AsyncMock(return_value=True)

        async def run():
            await manager.setup_branch(branch="feature/my-pr")

        asyncio.run(run())
        manager._checkout_ci_branch.assert_called_once_with("feature/my-pr")

    def test_unit_ci_branch_tries_delete_existing_on_rerun(self):
        """_checkout_ci_branch should try to delete existing branch (handles re-runs)."""
        manager, client = self._make_manager()

        async def run():
            await manager._checkout_ci_branch("feature/my-pr")

        asyncio.run(run())
        # Should attempt to delete existing branch before creating
        client.delete_branch.assert_called_with("test_project", "ci-validation-12345678")


# ---------------------------------------------------------------------------
# Integration tests — require real Looker credentials in env vars
# ---------------------------------------------------------------------------

def _skip_if_no_credentials():
    """Skip integration tests if credentials are not set."""
    required = ["LOOKER_BASE_URL", "LOOKER_CLIENT_ID", "LOOKER_CLIENT_SECRET", "LOOKER_PROJECT"]
    missing = [v for v in required if not os.environ.get(v)]
    if missing:
        raise unittest.SkipTest(
            f"Integration test skipped — set env vars: {', '.join(missing)}"
        )


class TestIntegrationBranchManager(unittest.TestCase):
    """Integration tests that connect to a real Looker instance."""

    def setUp(self):
        _skip_if_no_credentials()
        self.base_url = os.environ["LOOKER_BASE_URL"]
        self.client_id = os.environ["LOOKER_CLIENT_ID"]
        self.client_secret = os.environ["LOOKER_CLIENT_SECRET"]
        self.project = os.environ["LOOKER_PROJECT"]

    def test_integration_connect_and_get_state(self):
        """Verify we can connect and read the current project state."""
        async def run():
            async with AsyncLookerClient(
                base_url=self.base_url,
                client_id=self.client_id,
                client_secret=self.client_secret,
            ) as client:
                manager = AsyncBranchManager(client=client, project=self.project)
                state = await manager.get_project_state()
                print(f"\n  Current state: workspace={state.workspace}, branch={state.branch}")
                self.assertIn(state.workspace, ("production", "dev"))
                self.assertTrue(len(state.branch) > 0)

        asyncio.run(run())

    def test_integration_find_personal_branch(self):
        """Verify we can find the API user's personal branch."""
        async def run():
            async with AsyncLookerClient(
                base_url=self.base_url,
                client_id=self.client_id,
                client_secret=self.client_secret,
            ) as client:
                manager = AsyncBranchManager(
                    client=client,
                    project=self.project,
                    use_personal_branch=True,
                )
                personal = await manager._get_personal_branch()
                print(f"\n  Personal branch: {personal}")
                self.assertTrue(personal.startswith("dev-"), f"Expected 'dev-' prefix, got '{personal}'")

        asyncio.run(run())

    def test_integration_personal_branch_checkout_and_restore(self):
        """Full round-trip: enter context, checkout personal branch with a
        real branch name, then verify cleanup restores original state.

        Set LOOKER_TEST_BRANCH to a branch that exists in your project,
        or this test will use the personal branch name itself.
        """
        test_branch = os.environ.get("LOOKER_TEST_BRANCH")
        if not test_branch:
            raise unittest.SkipTest(
                "Set LOOKER_TEST_BRANCH to a branch name to test personal branch checkout"
            )

        async def run():
            async with AsyncLookerClient(
                base_url=self.base_url,
                client_id=self.client_id,
                client_secret=self.client_secret,
            ) as client:
                manager = AsyncBranchManager(
                    client=client,
                    project=self.project,
                    use_personal_branch=True,
                )

                # Save initial state
                initial_state = await manager.get_project_state()
                print(f"\n  Initial state: workspace={initial_state.workspace}, branch={initial_state.branch}")

                # Enter context manager
                async with manager:
                    # This is the call that was failing with 422 before the fix
                    await manager.setup_branch(branch=test_branch)

                    current_state = await manager.get_project_state()
                    print(f"  After setup: workspace={current_state.workspace}, branch={current_state.branch}")
                    self.assertEqual(current_state.workspace, "dev")

                # After exiting context, state should be restored
                restored_state = await manager.get_project_state()
                print(f"  After cleanup: workspace={restored_state.workspace}, branch={restored_state.branch}")

        asyncio.run(run())


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