import importlib.util
import sys
import unittest
from pathlib import Path
from unittest.mock import patch


def load_package():
    package_dir = Path(__file__).resolve().parents[1] / "kocoafab-robodog"
    init_py = package_dir / "__init__.py"
    spec = importlib.util.spec_from_file_location(
        "kocoafab_robodog",
        init_py,
        submodule_search_locations=[str(package_dir)],
    )
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    robodog_module = sys.modules["kocoafab_robodog.robodog"]
    return module, robodog_module


class ParallelDispatchThread:
    instances = []
    events = []

    def __init__(self, target=None, args=(), daemon=None, name=None):
        self.target = target
        self.args = args
        self.daemon = daemon
        self.name = name
        self.started = False
        self.finished = False
        ParallelDispatchThread.instances.append(self)

    def start(self):
        self.started = True
        ParallelDispatchThread.events.append("start")

    def join(self, timeout=None):
        ParallelDispatchThread.events.append("join")
        if not self.finished and self.target is not None:
            self.target(*self.args)
            self.finished = True

    def is_alive(self):
        return self.started and not self.finished


class RecorderDog:
    def __init__(self):
        self.calls = []

    def rotate(self, *args):
        self.calls.append(("rotate", args))

    def rotate_absolute(self, *args):
        self.calls.append(("rotate_absolute", args))

    def rotate_relative(self, *args):
        self.calls.append(("rotate_relative", args))

    def turn(self, *args):
        self.calls.append(("turn", args))

    def turn_absolute(self, *args):
        self.calls.append(("turn_absolute", args))

    def turn_relative(self, *args):
        self.calls.append(("turn_relative", args))


class MultiRotationApiTest(unittest.TestCase):
    def setUp(self):
        ParallelDispatchThread.instances.clear()
        ParallelDispatchThread.events.clear()

    def test_multi_rotate_helpers_start_all_threads_before_joining(self):
        package, robodog = load_package()
        dogs = [RecorderDog(), RecorderDog(), RecorderDog()]

        with patch.object(robodog, "Thread", ParallelDispatchThread):
            package.multi_rotate(dogs, 180, 70)
            package.multi_rotate_absolute(dogs, 90, 50)
            package.multi_rotate_relative(dogs, package.CCW, 45, 30)

        self.assertEqual(
            ["start", "start", "start", "join", "join", "join"] * 3,
            ParallelDispatchThread.events,
        )
        for dog in dogs:
            self.assertEqual(
                [
                    ("rotate", (180, 70)),
                    ("rotate_absolute", (90, 50)),
                    ("rotate_relative", (package.CCW, 45, 30)),
                ],
                dog.calls,
            )

    def test_multi_turn_helpers_are_exported_and_forward_calls(self):
        package, _robodog = load_package()
        dogs = [RecorderDog(), RecorderDog()]

        package.multi_turn(dogs, 180, 70)
        package.multi_turn_absolute(dogs, 90, 50)
        package.multi_turn_relative(dogs, package.CCW, 45, 30)

        for dog in dogs:
            self.assertEqual(
                [
                    ("turn", (180, 70)),
                    ("turn_absolute", (90, 50)),
                    ("turn_relative", (package.CCW, 45, 30)),
                ],
                dog.calls,
            )


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