"""Search-list and default-keychain behavior of the throwaway CI keychain.

The regression these tests pin down: setup used to run
``security default-keychain -s <ci keychain>``. On a persistent
(self-hosted) Mac the throwaway keychain's temp dir is reclaimed after
the run, the user default dangles, and macOS pops a
"Keychain ... cannot be found to store" dialog at every credential
write. Setup must never claim the default and must prune search-list
entries whose file is gone.
"""

import unittest
from unittest import mock

import keychain


class _SecurityHost:
    """Record ``security`` invocations against a fake filesystem."""

    def __init__(self, existing_files, search_list, default=""):
        self.files = set(existing_files)
        self.search_list = list(search_list)
        self.default = default
        self.calls = []

    def exists(self, path):
        return path in self.files

    def check_output(self, cmd):
        self.calls.append(cmd)
        assert cmd == ["security", "list-keychains", "-d", "user"]
        return "".join(f'    "{p}"\n' for p in self.search_list).encode()

    def check_call(self, cmd):
        self.calls.append(cmd)

    def run(self, cmd, capture_output=False, text=False):
        self.calls.append(cmd)
        assert cmd == ["security", "default-keychain"]
        if not self.default:
            return mock.Mock(returncode=1, stdout="", stderr="no default set")
        return mock.Mock(returncode=0, stdout=f'    "{self.default}"\n', stderr="")

    def patches(self):
        return (
            mock.patch.object(keychain.subprocess, "check_output", self.check_output),
            mock.patch.object(keychain.subprocess, "check_call", self.check_call),
            mock.patch.object(keychain.subprocess, "run", self.run),
            mock.patch.object(keychain.os.path, "exists", self.exists),
            mock.patch.object(
                keychain.os.path, "expanduser", lambda p: p.replace("~", "/Users/ci")
            ),
        )


LOGIN = "/Users/ci/Library/Keychains/login.keychain-db"
CI_KEYCHAIN = "/tmp/runner/ci.keychain-db"


def _apply(host, fn, *args):
    patches = host.patches()
    for p in patches:
        p.start()
    try:
        return fn(*args)
    finally:
        for p in patches:
            p.stop()


class PrependToUserSearchListTests(unittest.TestCase):
    def test_never_claims_the_default_keychain(self):
        host = _SecurityHost(
            existing_files={LOGIN, CI_KEYCHAIN},
            search_list=[LOGIN],
            default=LOGIN,
        )
        _apply(host, keychain._prepend_to_user_search_list, CI_KEYCHAIN)
        claimed = [c for c in host.calls if c[:2] == ["security", "default-keychain"] and "-s" in c]
        self.assertEqual(claimed, [])
        self.assertIn(
            ["security", "list-keychains", "-d", "user", "-s", CI_KEYCHAIN, LOGIN],
            host.calls,
        )

    def test_prunes_dangling_search_list_entries(self):
        stale = "/private/var/tmp/app-robot-task-1/ci.keychain-db"
        host = _SecurityHost(
            existing_files={LOGIN, CI_KEYCHAIN},
            search_list=[stale, LOGIN],
            default=LOGIN,
        )
        _apply(host, keychain._prepend_to_user_search_list, CI_KEYCHAIN)
        self.assertIn(
            ["security", "list-keychains", "-d", "user", "-s", CI_KEYCHAIN, LOGIN],
            host.calls,
        )
        for call in host.calls:
            self.assertNotIn(stale, call)

    def test_own_path_not_duplicated_when_already_listed(self):
        host = _SecurityHost(
            existing_files={LOGIN, CI_KEYCHAIN},
            search_list=[CI_KEYCHAIN, LOGIN],
            default=LOGIN,
        )
        _apply(host, keychain._prepend_to_user_search_list, CI_KEYCHAIN)
        self.assertIn(
            ["security", "list-keychains", "-d", "user", "-s", CI_KEYCHAIN, LOGIN],
            host.calls,
        )


class RepairDefaultKeychainTests(unittest.TestCase):
    def test_resets_a_dangling_default(self):
        host = _SecurityHost(
            existing_files={LOGIN},
            search_list=[LOGIN],
            default="/private/var/tmp/app-robot-task-1/ci.keychain-db",
        )
        _apply(host, keychain._repair_default_keychain)
        self.assertIn(["security", "default-keychain", "-s", LOGIN], host.calls)

    def test_sets_login_when_no_default_exists(self):
        host = _SecurityHost(existing_files={LOGIN}, search_list=[LOGIN], default="")
        _apply(host, keychain._repair_default_keychain)
        self.assertIn(["security", "default-keychain", "-s", LOGIN], host.calls)

    def test_leaves_a_healthy_default_alone(self):
        other = "/Users/ci/Library/Keychains/custom.keychain-db"
        host = _SecurityHost(
            existing_files={LOGIN, other}, search_list=[LOGIN], default=other
        )
        _apply(host, keychain._repair_default_keychain)
        self.assertNotIn(["security", "default-keychain", "-s", LOGIN], host.calls)

    def test_noop_when_login_keychain_is_absent(self):
        host = _SecurityHost(existing_files=set(), search_list=[], default="")
        _apply(host, keychain._repair_default_keychain)
        self.assertEqual(host.calls, [])


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