"""
Direct unit tests for the MVC Controller layer (e4s_cl/mvc/controller.py).

Rather than testing through the CLI, these tests drive Controller methods
directly via the Profile model and the test-configured USER_STORAGE so that
each method is exercised in isolation.  setUp/tearDown reset storage before
and after every case to keep tests independent.
"""

import tests
from e4s_cl.model.profile import Profile
from e4s_cl.error import UniqueAttributeError, ModelError


class ControllerCRUDTest(tests.TestCase):
    """create / one / all / count / exists / delete."""

    def setUp(self):
        self.resetStorage()
        self._ctrl = Profile.controller()

    def tearDown(self):
        self._ctrl.unselect()
        self.resetStorage()

    # convenience helper -----------------------------------------------
    def _make(self, name, **kwargs):
        return self._ctrl.create({'name': name, **kwargs})

    # ------------------------------------------------------------------

    def test_create_and_one_by_name(self):
        self._make('p1')
        result = self._ctrl.one({'name': 'p1'})
        self.assertIsNotNone(result)
        self.assertEqual(result['name'], 'p1')

    def test_one_missing_returns_none(self):
        self.assertIsNone(self._ctrl.one({'name': 'noexist'}))

    def test_all_empty(self):
        self.assertEqual(self._ctrl.all(), [])

    def test_all_returns_all_created(self):
        self._make('a')
        self._make('b')
        names = {p['name'] for p in self._ctrl.all()}
        self.assertEqual(names, {'a', 'b'})

    def test_count_empty(self):
        self.assertEqual(self._ctrl.count(), 0)

    def test_count_reflects_inserts(self):
        self._make('c1')
        self.assertEqual(self._ctrl.count(), 1)
        self._make('c2')
        self.assertEqual(self._ctrl.count(), 2)

    def test_exists_true(self):
        self._make('ex')
        self.assertTrue(self._ctrl.exists({'name': 'ex'}))

    def test_exists_false(self):
        self.assertFalse(self._ctrl.exists({'name': 'nx'}))

    def test_delete_removes_record(self):
        self._make('del_me')
        self._ctrl.delete({'name': 'del_me'})
        self.assertIsNone(self._ctrl.one({'name': 'del_me'}))

    def test_unique_constraint_on_create(self):
        """Creating two records with the same primary key raises UniqueAttributeError."""
        self._make('unique')
        with self.assertRaises(UniqueAttributeError):
            self._make('unique')


class ControllerSearchMatchTest(tests.TestCase):
    """search / match."""

    def setUp(self):
        self.resetStorage()
        self._ctrl = Profile.controller()
        self._ctrl.create({'name': 'docker_profile', 'backend': 'docker'})
        self._ctrl.create({'name': 'podman_profile', 'backend': 'podman'})
        self._ctrl.create({'name': 'apptainer_profile', 'backend': 'apptainer'})

    def tearDown(self):
        self._ctrl.unselect()
        self.resetStorage()

    def test_search_by_backend(self):
        results = self._ctrl.search({'backend': 'docker'})
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0]['name'], 'docker_profile')

    def test_search_no_match_returns_empty(self):
        results = self._ctrl.search({'backend': 'shifter'})
        self.assertEqual(results, [])

    def test_match_by_name_prefix_regex(self):
        results = self._ctrl.match('name', regex='^docker')
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0]['name'], 'docker_profile')

    def test_match_by_name_ends_with(self):
        # TinyDB uses re.match() which anchors at the start, so use a test
        # callable to check suffix membership instead of a bare suffix regex.
        results = self._ctrl.match('name',
                                   test=lambda v: v.endswith('_profile'))
        self.assertEqual(len(results), 3)

    def test_match_by_test_function(self):
        results = self._ctrl.match('backend',
                                   test=lambda v: v in {'docker', 'podman'})
        names = {r['name'] for r in results}
        self.assertEqual(names, {'docker_profile', 'podman_profile'})


class ControllerUpdateTest(tests.TestCase):
    """update / unset."""

    def setUp(self):
        self.resetStorage()
        self._ctrl = Profile.controller()

    def tearDown(self):
        self._ctrl.unselect()
        self.resetStorage()

    def test_update_changes_field(self):
        self._ctrl.create({'name': 'upd', 'backend': 'singularity'})
        self._ctrl.update({'backend': 'apptainer'}, {'name': 'upd'})
        result = self._ctrl.one({'name': 'upd'})
        self.assertEqual(result['backend'], 'apptainer')

    def test_update_nonexistent_raises_model_error(self):
        with self.assertRaises(ModelError):
            self._ctrl.update({'backend': 'x'}, {'name': 'ghost'})

    def test_update_invalid_attribute_raises_model_error(self):
        self._ctrl.create({'name': 'p'})
        with self.assertRaises(ModelError):
            self._ctrl.update({'nonexistent_attr': 'val'}, {'name': 'p'})

    def test_unset_removes_field(self):
        self._ctrl.create({'name': 'p', 'backend': 'docker', 'image': 'foo.sif'})
        self._ctrl.unset(['image'], {'name': 'p'})
        result = self._ctrl.one({'name': 'p'})
        self.assertNotIn('image', result)

    def test_unset_invalid_attr_raises_model_error(self):
        self._ctrl.create({'name': 'q'})
        with self.assertRaises(ModelError):
            self._ctrl.unset(['completely_bogus_field'], {'name': 'q'})


class ControllerTopicBusTest(tests.TestCase):
    """push_to_topic / pop_topic message bus."""

    def test_push_and_pop(self):
        Profile.controller().push_to_topic('t', 'msg1')
        Profile.controller().push_to_topic('t', 'msg2')
        msgs = Profile.controller().pop_topic('t')
        self.assertIn('msg1', msgs)
        self.assertIn('msg2', msgs)

    def test_second_pop_returns_empty(self):
        Profile.controller().push_to_topic('t2', 'x')
        Profile.controller().pop_topic('t2')
        self.assertEqual(Profile.controller().pop_topic('t2'), [])

    def test_pop_nonexistent_topic_returns_empty(self):
        self.assertEqual(Profile.controller().pop_topic('no_such_topic'), [])
