# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Unit tests for cloud computing provisioning backend functions."""

import datetime
from collections.abc import Generator
from contextlib import contextmanager
from unittest import mock

from django.utils import timezone

from debusine.db.models import Worker, WorkerPool
from debusine.server.provisioning import (
    create_new_workers,
    provision,
    terminate_idle,
)
from debusine.test.django import TestCase


class ProvisionTests(TestCase):
    """Test :py:func:`provision`."""

    def test_no_idle(self) -> None:
        """Call create_new_workers if there are no idle workers."""
        with (
            mock.patch(
                "debusine.server.provisioning.terminate_idle",
                return_value=False,
            ) as ti,
            mock.patch(
                "debusine.server.provisioning.create_new_workers"
            ) as cnw,
        ):
            provision()
        ti.assert_called()
        cnw.assert_called()

    def test_has_idle(self) -> None:
        """No not call create_new_workers if there are idle workers."""
        with (
            mock.patch(
                "debusine.server.provisioning.terminate_idle", return_value=True
            ) as ti,
            mock.patch(
                "debusine.server.provisioning.create_new_workers"
            ) as cnw,
        ):
            provision()
        ti.assert_called()
        cnw.assert_not_called()


class TerminateIdleTests(TestCase):
    """Test :py:func:`terminate_idle`."""

    @contextmanager
    def mock_terminate(self) -> Generator[mock.MagicMock, None, None]:
        """Mock WorkerPool.terminate_idle."""
        with mock.patch.object(
            WorkerPool,
            "terminate_worker",
            autospec=True,
        ) as term:
            yield term

    def _create_worker(
        self, idle_minutes: int, worker_pool: WorkerPool | None = None
    ) -> Worker:
        worker = self.playground.create_worker(worker_pool=worker_pool)
        worker.registered_at = timezone.now() - datetime.timedelta(
            seconds=idle_minutes * 60
        )
        worker.instance_created_at = timezone.now() - datetime.timedelta(
            seconds=idle_minutes * 60
        )
        worker.save()
        return worker

    def test_no_workers(self) -> None:
        """Call with no workers at all."""
        with self.mock_terminate() as term:
            self.assertFalse(terminate_idle())
        term.assert_not_called()

    def test_no_idle(self) -> None:
        """Call with no idle workers."""
        worker_pool = self.playground.create_worker_pool("test")
        self._create_worker(idle_minutes=1, worker_pool=None)
        self._create_worker(idle_minutes=1, worker_pool=worker_pool)
        with self.mock_terminate() as term:
            self.assertFalse(terminate_idle(minutes=10))
        term.assert_not_called()

    def test_one_static_idle(self) -> None:
        """Call with one idle static worker."""
        worker_pool = self.playground.create_worker_pool("test")
        self._create_worker(idle_minutes=11, worker_pool=None)
        self._create_worker(idle_minutes=1, worker_pool=worker_pool)
        with self.mock_terminate() as term:
            self.assertFalse(terminate_idle(minutes=10))
        term.assert_not_called()

    def test_one_dynamic_idle(self) -> None:
        """Call with one idle cloud worker."""
        worker_pool = self.playground.create_worker_pool("test")
        self._create_worker(idle_minutes=1, worker_pool=None)
        cloud = self._create_worker(idle_minutes=11, worker_pool=worker_pool)
        with self.mock_terminate() as term:
            self.assertTrue(terminate_idle(minutes=10))
        term.assert_called_with(worker_pool, cloud)


class CreateNewWorkersTests(TestCase):
    """Test :py:func:`create_new_workers`."""

    def test_noop(self) -> None:
        """Call the function: it does nothing at the moment."""
        create_new_workers()
