# Copyright 2026 Cloudbase Solutions Srl
# All Rights Reserved.

"""Integration tests for the OS morphing deployments.

Exercises deployments with skip_os_morphing=False, OS detection, and package
installation in the target OS.
"""

import os
import re
import unittest
import uuid

from coriolis.db import api as db_api
from coriolis.tests.integration import base as integration_base
from coriolis.tests.integration import harness as integration_harness
from coriolis.tests.integration import osmorphing_utils


class OsMorphingDeploymentTestBase(
        integration_base.ReplicaIntegrationTestBase):

    # NOTE(claudiub): Size must be high enough to contain the tested OS and
    # any new packages to be added during OS morphing.
    _SCSI_DEBUG_SIZE_MB = 256

    @classmethod
    def setUpClass(cls):
        harness = integration_harness._IntegrationHarness.get()
        if not harness.uses_core_test_import_provider():
            raise unittest.SkipTest(
                "OS morphing tests require local disk access")
        super().setUpClass()

    def setUp(self):
        super().setUp()
        osmorphing_utils.write_os_image_to_disk(
            self._src_device, "ubuntu:24.04")


class OsMorphingDeploymentTest(OsMorphingDeploymentTestBase):
    def test_deployment_with_os_morphing(self):
        self.assertFalse(
            osmorphing_utils.path_exists_on_device(
                self._src_device, "usr/bin/jq"),
            "jq was found on the source device before OS morphing",
        )

        self._execute_transfer_and_deployment()

        self.assertTrue(
            osmorphing_utils.path_exists_on_device(
                self._dst_device, "usr/bin/jq"),
            "jq was not found on the destination device after OS morphing",
        )

    def test_os_morphing_global_script_basic_format(self):
        expected_string = str(uuid.uuid4())
        user_scripts = {
            'global': {
                # Coriolis will pass the root path as the first argument.
                # We'll use a Windows style line ending, expecting it to
                # be sanitized.
                'linux': f"echo -n {expected_string} > $1/cookie\r\n",
                'windows': 'should-not-get-executed',
            }
        }
        deployment_kwargs = {
            "user_scripts": user_scripts,
        }
        self._execute_transfer_and_deployment(deployment_kwargs)

        file_contents = osmorphing_utils.read_file_from_device(
            self._dst_device,
            "cookie")
        self.assertEqual(expected_string, file_contents)

    def test_os_morphing_instance_script_basic_format(self):
        expected_string = str(uuid.uuid4())
        user_scripts = {
            'instances': {
                self._instance_name: (
                    f"echo -n {expected_string} > $1/cookie\n\r")
            }
        }
        deployment_kwargs = {
            "user_scripts": user_scripts,
        }
        self._execute_transfer_and_deployment(deployment_kwargs)

        file_contents = osmorphing_utils.read_file_from_device(
            self._dst_device,
            "cookie")
        self.assertEqual(expected_string, file_contents)

    def test_os_morphing_global_script_extended_format(self):
        user_scripts = {
            'global': {
                'linux': [
                    {
                        "phase": "osmorphing_pre_os_mount",
                        # Write the mounts to the minion temp dir.
                        "payload": "mount > /tmp/pre_mounts\r\n",
                    },
                    {
                        "phase": "osmorphing_post_os_mount",
                        # Copy the mounts file to the migrated OS drive.
                        "payload": "cp /tmp/pre_mounts $1/",
                    },
                    {
                        "phase": "osmorphing_post_os_mount",
                        # Write the mounts to the migrated OS drive.
                        "payload": "mount > $1/post_mounts",
                    },
                ],
                'windows': [
                    {
                        "phase": "osmorphing_pre_os_mount",
                        "payload": "should-not-get-executed",
                    },
                    {
                        "phase": "osmorphing_post_os_mount",
                        "payload": "should-not-get-executed",
                    },
                ]
            }
        }
        deployment_kwargs = {
            "user_scripts": user_scripts,
        }
        self._execute_transfer_and_deployment(deployment_kwargs)

        pre_mounts = osmorphing_utils.read_file_from_device(
            self._dst_device,
            "pre_mounts")
        post_mounts = osmorphing_utils.read_file_from_device(
            self._dst_device,
            "post_mounts")

        # Ensure that the "osmorphing_pre_os_mount" was executed before
        # the replica OS disk was mounted.
        self.assertNotIn(self._dst_device, pre_mounts)
        self.assertIn(self._dst_device, post_mounts)

    def test_os_morphing_global_script_first_boot(self):
        payload = "mount > /boot_mounts"
        user_scripts = {
            'global': {
                'linux': [
                    {
                        "phase": "replica_first_boot",
                        "payload": "mount > /boot_mounts",
                    },
                ],
                'windows': [
                    {
                        "phase": "replica_first_boot",
                        "payload": "should-not-get-executed",
                    },
                ]
            }
        }
        deployment_kwargs = {
            "user_scripts": user_scripts,
        }
        self._execute_transfer_and_deployment(deployment_kwargs)

        # TODO(lpetrut): the test import provider doesn't actually create
        # replica instances (containers). If it did, we'd have no way to clean
        # them up using Coriolis APIs.
        #
        # For this reason, we can't ensure that the first boot scripts
        # actually get executed. We'll merely verify that those files
        # have been injected at the expected location.
        first_boot_script_dir = "usr/lib/coriolis/firstboot/user"
        first_boot_scripts = osmorphing_utils.list_files_from_device(
            self._dst_device, first_boot_script_dir)
        if not first_boot_scripts:
            raise AssertionError("Couldn't find first boot script dir.")

        found = False
        for file_name in first_boot_scripts:
            if re.match(r"\d+-\w+\.sh", file_name):
                first_boot_script_path = os.path.join(
                    first_boot_script_dir, file_name)
                first_boot_script = osmorphing_utils.read_file_from_device(
                    self._dst_device,
                    first_boot_script_path)
                if payload == first_boot_script:
                    found = True
                if payload == "should-not-get-executed":
                    raise AssertionError(
                        "Linux instance contains Windows script.")

        if not found:
            raise AssertionError(
                "Couldn't find the expected first boot script.")


class OsMorphingMinionPoolDeploymentTest(
        integration_base.MinionPoolTestBase, OsMorphingDeploymentTestBase):
    """OS morphing deployment using a minion pool for the OS morphing phase."""

    _CREATE_MINION_POOLS = True

    def test_deployment_with_os_morphing(self):
        self.assertFalse(
            osmorphing_utils.path_exists_on_device(
                self._src_device, "usr/bin/jq"),
            "jq was found on the source device before OS morphing",
        )

        deployment_kwargs = {
            "instance_osmorphing_minion_pool_mappings": {
                self._instance_name: self._pool_id,
            },
        }
        self._execute_transfer_and_deployment(deployment_kwargs)

        self.assertTrue(
            osmorphing_utils.path_exists_on_device(
                self._dst_device, "usr/bin/jq"),
            "jq was not found on the destination device after OS morphing",
        )

        ctxt = self._get_db_context()
        pool = db_api.get_minion_pool(
            ctxt, self._pool_id, include_machines=True)
        self.assertTrue(
            pool.minion_machines,
            "OS morphing pool has no minion machines")

        for machine in pool.minion_machines:
            self.assertIsNotNone(
                machine.last_used_at,
                "OS morphing minion machine %s was never used" % machine.id,
            )
