#!/usr/bin/python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: GPL-2.0-only
"""
Test suite for tuna cpuset CLI commands

Tests the command-line interface functions added to tuna-cmd.py:
- cpuset create (with auto-naming, custom names, --isolated)
- cpuset list (with --pattern, --verbose, --skip-empty)
- cpuset destroy (single and pattern-based, with --skip-empty)
- cpuset move (moving processes to cpusets)
- cpuset show (detailed information about a specific cpuset)
- cpuset status (system-wide overview of all cpusets)
- get_next_tuna_cpuset_name() auto-naming logic

All tests clean up created cpusets afterwards (even though tuna creates persistent cpusets).
"""

import unittest
import os
import sys
import subprocess
from pathlib import Path

# Add tuna to path if running from tests directory
if Path(__file__).parent.name == 'tests':
    sys.path.insert(0, str(Path(__file__).parent.parent))

from tuna import cpuset


# Import the CLI functions from tuna-cmd.py
# We need to import from the parent directory
tuna_cmd_path = Path(__file__).parent.parent / 'tuna-cmd.py'
spec = __import__('importlib.util').util.spec_from_file_location("tuna_cmd", tuna_cmd_path)
tuna_cmd = __import__('importlib.util').util.module_from_spec(spec)
spec.loader.exec_module(tuna_cmd)

# Import the CLI functions we want to test
get_next_tuna_cpuset_name = tuna_cmd.get_next_tuna_cpuset_name
cpuset_create = tuna_cmd.cpuset_create
cpuset_list = tuna_cmd.cpuset_list
cpuset_destroy = tuna_cmd.cpuset_destroy
cpuset_move = tuna_cmd.cpuset_move
cpuset_show = tuna_cmd.cpuset_show
cpuset_status = tuna_cmd.cpuset_status
get_cpuset_info = tuna_cmd.get_cpuset_info
get_all_cpusets_info = tuna_cmd.get_all_cpusets_info


@unittest.skipUnless(os.geteuid() == 0, "Requires root permissions")
@unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
class TestGetNextTunaCpusetName(unittest.TestCase):
    """Test auto-naming logic for tuna cpusets"""

    def setUp(self):
        """Clean up any existing tuna cpusets before testing"""
        self.ci = cpuset.CpusetsInit()
        try:
            cpuset.cleanup_cpusets('tuna[0-9]*', force=True, recursive=False)
        except:
            pass

    def tearDown(self):
        """Clean up tuna cpusets after testing"""
        try:
            cpuset.cleanup_cpusets('tuna[0-9]*', force=True, recursive=False)
        except:
            pass

    def test_first_name_is_tuna0(self):
        """Test that first auto-generated name is 'tuna0'"""
        name = get_next_tuna_cpuset_name()
        self.assertEqual(name, 'tuna0')

    def test_sequential_naming(self):
        """Test that names increment sequentially"""
        # Create tuna0
        cs0 = cpuset.Cpuset('tuna0')
        cs0.write_memnode('0')

        name = get_next_tuna_cpuset_name()
        self.assertEqual(name, 'tuna1')

        # Create tuna1
        cs1 = cpuset.Cpuset('tuna1')
        cs1.write_memnode('0')

        name = get_next_tuna_cpuset_name()
        self.assertEqual(name, 'tuna2')

    def test_fills_gaps(self):
        """Test that naming fills gaps in sequence"""
        # Create tuna0 and tuna2 (skip tuna1)
        cs0 = cpuset.Cpuset('tuna0')
        cs2 = cpuset.Cpuset('tuna2')
        cs0.write_memnode('0')
        cs2.write_memnode('0')

        # Should return tuna1 (the gap)
        name = get_next_tuna_cpuset_name()
        self.assertEqual(name, 'tuna1')


@unittest.skipUnless(os.geteuid() == 0, "Requires root permissions")
@unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
class TestCpusetCreateCLI(unittest.TestCase):
    """Test 'tuna cpuset create' command"""

    def setUp(self):
        """Set up test fixtures"""
        self.ci = cpuset.CpusetsInit()
        self.created_cpusets = []

    def tearDown(self):
        """Clean up all created cpusets"""
        for name in self.created_cpusets:
            try:
                cpuset.destroy_cpuset(name, force=True)
            except:
                pass

    def test_create_with_auto_naming(self):
        """Test creating cpuset with auto-generated name"""
        # Clean up first
        try:
            cpuset.cleanup_cpusets('tuna[0-9]*', force=True, recursive=False)
        except:
            pass

        # Create with auto-naming
        cpuset_create(cpu_list=[0, 1], name=None, isolated=False)
        self.created_cpusets.append('tuna0')

        # Verify it exists
        self.assertTrue(os.path.exists('/sys/fs/cgroup/tuna0'))

        # Verify CPUs are assigned
        with open('/sys/fs/cgroup/tuna0/cpuset.cpus') as f:
            cpus = f.read().strip()
        self.assertIn('0', cpus)
        self.assertIn('1', cpus)

    def test_create_with_custom_name(self):
        """Test creating cpuset with custom name"""
        cpuset_create(cpu_list=[2, 3], name='my_cpuset', isolated=False)
        self.created_cpusets.append('my_cpuset')

        # Verify it exists
        self.assertTrue(os.path.exists('/sys/fs/cgroup/my_cpuset'))

        # Verify CPUs are assigned
        with open('/sys/fs/cgroup/my_cpuset/cpuset.cpus') as f:
            cpus = f.read().strip()
        self.assertIn('2', cpus)
        self.assertIn('3', cpus)

    def test_create_with_isolated(self):
        """Test creating cpuset with --isolated flag"""
        cpuset_create(cpu_list=[0], name='isolated_cpuset', isolated=True)
        self.created_cpusets.append('isolated_cpuset')

        # Verify partition type (if supported)
        partition_file = '/sys/fs/cgroup/isolated_cpuset/cpuset.cpus.partition'
        if os.path.exists(partition_file):
            with open(partition_file) as f:
                partition = f.read().strip()
            # Should be 'isolated' (or possibly 'root'/'member' if not supported)
            self.assertIn(partition, ['isolated', 'root', 'member'])

    def test_create_with_numa_autodetect(self):
        """Test creating cpuset with NUMA auto-detection (no --memory-nodes)"""
        cpuset_create(cpu_list=[0, 1], name='test_numa_auto', isolated=False, memory_nodes=None)
        self.created_cpusets.append('test_numa_auto')

        # Verify cpuset exists
        self.assertTrue(os.path.exists('/sys/fs/cgroup/test_numa_auto'))

        # Verify memory nodes are set (should be auto-detected)
        with open('/sys/fs/cgroup/test_numa_auto/cpuset.mems') as f:
            mems = f.read().strip()
        # Should have at least one memory node set
        self.assertTrue(len(mems) > 0)
        # On single-node system, should be "0"
        # On multi-node, could be "0", "1", "0-1", etc.
        self.assertRegex(mems, r'^[0-9,-]+$')

    def test_create_with_numa_manual(self):
        """Test creating cpuset with manual --memory-nodes override"""
        cpuset_create(cpu_list=[0, 1], name='test_numa_manual', isolated=False, memory_nodes='0')
        self.created_cpusets.append('test_numa_manual')

        # Verify memory nodes are set to what we specified
        with open('/sys/fs/cgroup/test_numa_manual/cpuset.mems') as f:
            mems = f.read().strip()
        self.assertEqual(mems, '0')


@unittest.skipUnless(os.geteuid() == 0, "Requires root permissions")
@unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
class TestCpusetListCLI(unittest.TestCase):
    """Test 'tuna cpuset list' command"""

    def setUp(self):
        """Set up test fixtures"""
        self.ci = cpuset.CpusetsInit()
        self.created_cpusets = []

        # Create some test cpusets
        cs1 = cpuset.Cpuset('test_list_1')
        cs1.write_memnode('0')
        cs1.assign_cpus('0-1')
        self.created_cpusets.append('test_list_1')

        cs2 = cpuset.Cpuset('test_list_2')
        cs2.write_memnode('0')
        cs2.assign_cpus('2-3')
        self.created_cpusets.append('test_list_2')

        # Create one without CPUs (empty)
        cs3 = cpuset.Cpuset('test_list_empty')
        cs3.write_memnode('0')
        self.created_cpusets.append('test_list_empty')

    def tearDown(self):
        """Clean up all created cpusets"""
        for name in self.created_cpusets:
            try:
                cpuset.destroy_cpuset(name, force=True)
            except:
                pass

    def test_list_basic(self):
        """Test basic cpuset list (should include our test cpusets)"""
        # Capture output by listing directly
        cpusets = cpuset.list_cpusets(recursive=False)

        # Should include our cpusets
        self.assertIn('test_list_1', cpusets)
        self.assertIn('test_list_2', cpusets)
        self.assertIn('test_list_empty', cpusets)

    def test_list_with_pattern(self):
        """Test cpuset list with --pattern filter"""
        cpusets = cpuset.list_cpusets(pattern='test_list_*', recursive=False)

        # Should include our cpusets
        self.assertIn('test_list_1', cpusets)
        self.assertIn('test_list_2', cpusets)
        self.assertIn('test_list_empty', cpusets)

    def test_list_default_skips_empty(self):
        """Test cpuset list default behavior (skips empty cpusets)"""
        # Get all cpusets matching pattern
        all_cpusets = cpuset.list_cpusets(pattern='test_list_*', recursive=False)

        # Filter out empty ones manually (simulating default behavior)
        non_empty = []
        for cs_name in all_cpusets:
            cs_path = os.path.join('/sys/fs/cgroup', cs_name)
            with open(os.path.join(cs_path, 'cpuset.cpus')) as f:
                cpus = f.read().strip()
                if cpus:
                    non_empty.append(cs_name)

        # Should include cpusets with CPUs
        self.assertIn('test_list_1', non_empty)
        self.assertIn('test_list_2', non_empty)

        # Should NOT include empty cpuset
        self.assertNotIn('test_list_empty', non_empty)


@unittest.skipUnless(os.geteuid() == 0, "Requires root permissions")
@unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
class TestCpusetDestroyCLI(unittest.TestCase):
    """Test 'tuna cpuset destroy' command"""

    def setUp(self):
        """Set up test fixtures"""
        self.ci = cpuset.CpusetsInit()
        self.created_cpusets = []

    def tearDown(self):
        """Clean up any remaining cpusets"""
        for name in self.created_cpusets:
            try:
                cpuset.destroy_cpuset(name, force=True)
            except:
                pass

    def test_destroy_single_cpuset(self):
        """Test destroying a single cpuset by name"""
        # Create a cpuset
        cs = cpuset.Cpuset('test_destroy_single')
        cs.write_memnode('0')
        self.created_cpusets.append('test_destroy_single')

        path = cs.cpuset_path
        self.assertTrue(os.path.exists(path))

        # Destroy it (without force, no tasks)
        cpuset_destroy(names=['test_destroy_single'], pattern=None, force=False,
                       include_empty=True, recursive=True)

        # Should be gone
        self.assertFalse(os.path.exists(path))
        self.created_cpusets.remove('test_destroy_single')

    def test_destroy_by_pattern(self):
        """Test destroying multiple cpusets by pattern"""
        # Create multiple cpusets
        cs1 = cpuset.Cpuset('test_destroy_bulk_1')
        cs1.write_memnode('0')
        cs2 = cpuset.Cpuset('test_destroy_bulk_2')
        cs2.write_memnode('0')
        self.created_cpusets.extend(['test_destroy_bulk_1', 'test_destroy_bulk_2'])

        # Destroy by pattern
        cpuset_destroy(names=None, pattern='test_destroy_bulk_*', force=True,
                       include_empty=True, recursive=False)

        # Should be gone
        self.assertFalse(os.path.exists(cs1.cpuset_path))
        self.assertFalse(os.path.exists(cs2.cpuset_path))
        self.created_cpusets = []

    def test_destroy_default_skips_empty(self):
        """Test destroying with default behavior (skips empty cpusets)"""
        # Create cpuset with CPUs
        cs1 = cpuset.Cpuset('test_destroy_nonempty')
        cs1.write_memnode('0')
        cs1.assign_cpus('0')
        self.created_cpusets.append('test_destroy_nonempty')

        # Create empty cpuset
        cs2 = cpuset.Cpuset('test_destroy_empty')
        cs2.write_memnode('0')
        self.created_cpusets.append('test_destroy_empty')

        # Destroy with default (include_empty=False, which skips empty)
        cpuset_destroy(names=None, pattern='test_destroy_*', force=True,
                       include_empty=False, recursive=False)

        # Non-empty should be destroyed
        self.assertFalse(os.path.exists(cs1.cpuset_path))
        self.created_cpusets.remove('test_destroy_nonempty')

        # Empty should still exist
        self.assertTrue(os.path.exists(cs2.cpuset_path))


@unittest.skipUnless(os.geteuid() == 0, "Requires root permissions")
@unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
class TestCpusetMoveCLI(unittest.TestCase):
    """Test 'tuna cpuset move' command"""

    def setUp(self):
        """Set up test fixtures"""
        self.ci = cpuset.CpusetsInit()
        self.created_cpusets = []

    def tearDown(self):
        """Clean up all created cpusets and move test process back to root"""
        # Move current process back to root
        try:
            root = cpuset.CpusetsInit()
            root.write_pid(os.getpid())
        except:
            pass

        # Clean up cpusets
        for name in self.created_cpusets:
            try:
                cpuset.destroy_cpuset(name, force=True)
            except:
                pass

    def test_move_with_pids(self):
        """Test moving processes with --pids argument"""
        # Create a cpuset
        cpuset_create(cpu_list=[0, 1], name='test_move_pids', isolated=False)
        self.created_cpusets.append('test_move_pids')

        # Move current process using pid_list (string format)
        current_pid = os.getpid()
        cpuset_move('test_move_pids', thread_list=None, pid_list=str(current_pid))

        # Verify PID is in the cpuset
        with open('/sys/fs/cgroup/test_move_pids/cgroup.procs') as f:
            pids = f.read().strip().split('\n')
        self.assertIn(str(current_pid), pids)

    def test_move_with_threads(self):
        """Test moving processes with --threads argument (numeric PIDs)"""
        # Create a cpuset
        cpuset_create(cpu_list=[0, 1], name='test_move_threads', isolated=False)
        self.created_cpusets.append('test_move_threads')

        # Move current process using thread_list (list of integers)
        current_pid = os.getpid()
        cpuset_move('test_move_threads', thread_list=[current_pid], pid_list=None)

        # Verify PID is in the cpuset
        with open('/sys/fs/cgroup/test_move_threads/cgroup.procs') as f:
            pids = f.read().strip().split('\n')
        self.assertIn(str(current_pid), pids)

    def test_move_both_pids_and_threads(self):
        """Test moving processes with both --pids and --threads"""
        # Create a cpuset
        cpuset_create(cpu_list=[0, 1], name='test_move_both', isolated=False)
        self.created_cpusets.append('test_move_both')

        # Move current process using both arguments
        current_pid = os.getpid()
        cpuset_move('test_move_both', thread_list=[current_pid], pid_list=str(current_pid))

        # Verify PID is in the cpuset (should only appear once despite being in both lists)
        with open('/sys/fs/cgroup/test_move_both/cgroup.procs') as f:
            pids = f.read().strip().split('\n')
        self.assertIn(str(current_pid), pids)

    def test_move_nonexistent_cpuset_error(self):
        """Test error when trying to move to nonexistent cpuset"""
        # Should exit with error (sys.exit(1))
        with self.assertRaises(SystemExit) as cm:
            cpuset_move('nonexistent_cpuset', thread_list=None, pid_list='1234')
        self.assertEqual(cm.exception.code, 1)

    def test_move_no_pids_error(self):
        """Test error when no PIDs are provided"""
        # Create a cpuset
        cpuset_create(cpu_list=[0, 1], name='test_move_nopids', isolated=False)
        self.created_cpusets.append('test_move_nopids')

        # Should exit with error when neither thread_list nor pid_list provided
        with self.assertRaises(SystemExit) as cm:
            cpuset_move('test_move_nopids', thread_list=None, pid_list=None)
        self.assertEqual(cm.exception.code, 2)

    def test_move_invalid_pid(self):
        """Test moving invalid PID (should fail gracefully)"""
        # Create a cpuset
        cpuset_create(cpu_list=[0, 1], name='test_move_invalid', isolated=False)
        self.created_cpusets.append('test_move_invalid')

        # Try to move non-existent PID - should exit with error since all moves failed
        with self.assertRaises(SystemExit) as cm:
            cpuset_move('test_move_invalid', thread_list=[999999], pid_list=None)
        self.assertEqual(cm.exception.code, 1)

    def test_move_blocklisted_process(self):
        """Test that blocklisted processes (like systemd) cannot be moved"""
        # Create a cpuset
        cpuset_create(cpu_list=[0, 1], name='test_move_blocklist', isolated=False)
        self.created_cpusets.append('test_move_blocklist')

        # Try to move PID 1 (systemd) - should be blocked and exit with error
        # since all moves failed (success_count == 0)
        with self.assertRaises(SystemExit) as cm:
            cpuset_move('test_move_blocklist', thread_list=None, pid_list='1')
        self.assertEqual(cm.exception.code, 1)

        # Verify PID 1 is NOT in the cpuset
        with open('/sys/fs/cgroup/test_move_blocklist/cgroup.procs') as f:
            pids = f.read().strip().split('\n')
        self.assertNotIn('1', pids, "PID 1 (systemd) should not be in the cpuset")

    def test_move_mixed_blocklisted_and_normal(self):
        """Test moving mix of blocklisted and normal processes"""
        # Create a cpuset
        cpuset_create(cpu_list=[0, 1], name='test_move_mixed', isolated=False)
        self.created_cpusets.append('test_move_mixed')

        # Try to move both PID 1 (blocklisted) and our own PID (normal)
        current_pid = os.getpid()
        cpuset_move('test_move_mixed', thread_list=None, pid_list=f'1,{current_pid}')

        # Verify PID 1 is NOT in the cpuset
        with open('/sys/fs/cgroup/test_move_mixed/cgroup.procs') as f:
            pids = f.read().strip().split('\n')
        self.assertNotIn('1', pids, "PID 1 should be blocked")
        # Our PID should be there
        self.assertIn(str(current_pid), pids, "Normal process should be moved")


@unittest.skipUnless(os.geteuid() == 0, "Requires root permissions")
@unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
class TestCpusetShowCLI(unittest.TestCase):
    """Test 'tuna cpuset show' command"""

    def setUp(self):
        """Create test cpusets before each test"""
        self.ci = cpuset.CpusetsInit()
        # Clean up any existing test cpusets
        try:
            cpuset.cleanup_cpusets('test_show_*', force=True, recursive=True)
        except:
            pass

    def tearDown(self):
        """Clean up test cpusets after each test"""
        try:
            cpuset.cleanup_cpusets('test_show_*', force=True, recursive=True)
        except:
            pass

    def test_show_basic_info(self):
        """Test showing basic cpuset information"""
        # Create a test cpuset
        cs = cpuset.Cpuset('test_show_basic')
        cs.write_memnode('0')
        cs.assign_cpus('0-1')

        # Get info using the helper function
        info = get_cpuset_info('test_show_basic')

        self.assertEqual(info['name'], 'test_show_basic')
        self.assertEqual(info['cpus'], '0-1')
        self.assertEqual(info['mems'], '0')
        self.assertIsNone(info['error'])

    def test_show_nonexistent_cpuset(self):
        """Test showing info for nonexistent cpuset"""
        # Try to show a cpuset that doesn't exist
        with self.assertRaises(SystemExit):
            # Capture stdout to avoid cluttering test output
            import io
            import contextlib
            f = io.StringIO()
            with contextlib.redirect_stderr(f):
                cpuset_show('test_show_nonexistent', show_tasks=False)

    def test_show_with_tasks(self):
        """Test showing cpuset with tasks"""
        # Create a test cpuset and add current process
        cs = cpuset.Cpuset('test_show_tasks')
        cs.write_memnode('0')
        cs.assign_cpus('0-1')
        cs.write_pid(os.getpid())

        # Get info
        info = get_cpuset_info('test_show_tasks')

        self.assertGreater(info['task_count'], 0)
        self.assertIn(str(os.getpid()), info['tasks'])

    def test_get_cpuset_info_error_handling(self):
        """Test get_cpuset_info with invalid cpuset"""
        info = get_cpuset_info('nonexistent_cpuset')

        self.assertIsNotNone(info['error'])
        self.assertEqual(info['name'], 'nonexistent_cpuset')


@unittest.skipUnless(os.geteuid() == 0, "Requires root permissions")
@unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
class TestCpusetStatusCLI(unittest.TestCase):
    """Test 'tuna cpuset status' command"""

    def setUp(self):
        """Create test cpusets before each test"""
        self.ci = cpuset.CpusetsInit()
        # Clean up any existing test cpusets
        try:
            cpuset.cleanup_cpusets('test_status_*', force=True, recursive=True)
        except:
            pass

    def tearDown(self):
        """Clean up test cpusets after each test"""
        try:
            cpuset.cleanup_cpusets('test_status_*', force=True, recursive=True)
        except:
            pass

    def test_status_with_multiple_cpusets(self):
        """Test status showing multiple cpusets"""
        # Create multiple test cpusets
        cs1 = cpuset.Cpuset('test_status_1')
        cs1.write_memnode('0')
        cs1.assign_cpus('0-1')

        cs2 = cpuset.Cpuset('test_status_2')
        cs2.write_memnode('0')
        cs2.assign_cpus('2-3')
        cs2.set_cpu_exclusive()

        # Get all cpusets info
        infos = get_all_cpusets_info(pattern='test_status_*', recursive=False)

        self.assertEqual(len(infos), 2)
        names = [info['name'] for info in infos]
        self.assertIn('test_status_1', names)
        self.assertIn('test_status_2', names)

    def test_status_with_pattern_filter(self):
        """Test status with pattern filtering"""
        # Create cpusets with different patterns
        cs1 = cpuset.Cpuset('test_status_match')
        cs1.write_memnode('0')
        cs1.assign_cpus('0')

        cs2 = cpuset.Cpuset('test_other_nomatch')
        cs2.write_memnode('0')
        cs2.assign_cpus('1')

        # Get cpusets matching pattern
        infos = get_all_cpusets_info(pattern='test_status_*', recursive=False)

        self.assertEqual(len(infos), 1)
        self.assertEqual(infos[0]['name'], 'test_status_match')

        # Clean up the non-matching one manually
        try:
            cpuset.destroy_cpuset('test_other_nomatch', force=True)
        except:
            pass

    def test_status_empty_filtering(self):
        """Test status filtering of empty cpusets"""
        # Create cpuset with CPUs
        cs1 = cpuset.Cpuset('test_status_filled')
        cs1.write_memnode('0')
        cs1.assign_cpus('0-1')

        # Create cpuset without CPUs
        cs2 = cpuset.Cpuset('test_status_empty')
        cs2.write_memnode('0')

        # Get all (should include both)
        all_infos = get_all_cpusets_info(pattern='test_status_*', skip_empty=False)
        self.assertEqual(len(all_infos), 2)

        # Get non-empty only
        nonempty_infos = get_all_cpusets_info(pattern='test_status_*', skip_empty=True)
        self.assertEqual(len(nonempty_infos), 1)
        self.assertEqual(nonempty_infos[0]['name'], 'test_status_filled')

    def test_status_shows_isolated(self):
        """Test status correctly identifies isolated cpusets"""
        # Create isolated cpuset
        cs = cpuset.Cpuset('test_status_isolated')
        cs.write_memnode('0')
        cs.assign_cpus('0-1')
        cs.set_cpu_exclusive()

        info = get_cpuset_info('test_status_isolated')

        self.assertEqual(info['partition'], 'isolated')


@unittest.skipUnless(os.geteuid() == 0, "Requires root permissions")
@unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
class TestCpusetModifyCLI(unittest.TestCase):
    """Test 'tuna cpuset modify' command"""

    def setUp(self):
        """Create test cpusets"""
        self.test_cpusets = []

    def tearDown(self):
        """Clean up test cpusets"""
        for cs_name in self.test_cpusets:
            try:
                cpuset.destroy_cpuset(cs_name, force=True)
            except:
                pass

    def test_modify_add_cpus(self):
        """Test modifying cpuset by adding CPUs"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_modify_add')
        cs.write_memnode('0')
        cs.assign_cpus('0-1')
        self.test_cpusets.append('test_modify_add')

        # Call modify command
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'cpuset', 'modify',
            'test_modify_add', '--add-cpus', '2-3'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0)
        self.assertIn('Modified cpuset', result.stdout)
        self.assertIn('CPUs:', result.stdout)

        # Verify CPUs were added
        from tuna import tuna
        cs_reload = cpuset.Cpuset('test_modify_add', existing_ok=True)
        cpus_str = cs_reload.get_cpus()
        cpu_list = tuna.expand_cpulist(cpus_str)

        self.assertIn(0, cpu_list)
        self.assertIn(1, cpu_list)
        self.assertIn(2, cpu_list)
        self.assertIn(3, cpu_list)

    def test_modify_remove_cpus(self):
        """Test modifying cpuset by removing CPUs"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_modify_remove')
        cs.write_memnode('0')
        cs.assign_cpus('0-3')
        self.test_cpusets.append('test_modify_remove')

        # Call modify command
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'cpuset', 'modify',
            'test_modify_remove', '--remove-cpus', '2-3'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0)
        self.assertIn('Modified cpuset', result.stdout)

        # Verify CPUs were removed
        from tuna import tuna
        cs_reload = cpuset.Cpuset('test_modify_remove', existing_ok=True)
        cpus_str = cs_reload.get_cpus()
        cpu_list = tuna.expand_cpulist(cpus_str)

        self.assertIn(0, cpu_list)
        self.assertIn(1, cpu_list)
        self.assertNotIn(2, cpu_list)
        self.assertNotIn(3, cpu_list)

    def test_modify_add_and_remove_cpus(self):
        """Test modifying cpuset by adding and removing CPUs simultaneously"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_modify_both')
        cs.write_memnode('0')
        cs.assign_cpus('0-3')
        self.test_cpusets.append('test_modify_both')

        # Call modify command
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'cpuset', 'modify',
            'test_modify_both', '--add-cpus', '4-5', '--remove-cpus', '0-1'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0)

        # Verify changes
        from tuna import tuna
        cs_reload = cpuset.Cpuset('test_modify_both', existing_ok=True)
        cpus_str = cs_reload.get_cpus()
        cpu_list = tuna.expand_cpulist(cpus_str)

        self.assertNotIn(0, cpu_list)
        self.assertNotIn(1, cpu_list)
        self.assertIn(2, cpu_list)
        self.assertIn(3, cpu_list)
        self.assertIn(4, cpu_list)
        self.assertIn(5, cpu_list)

    def test_modify_memory_nodes(self):
        """Test modifying memory nodes"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_modify_mems')
        cs.write_memnode('0')
        cs.assign_cpus('0-1')
        self.test_cpusets.append('test_modify_mems')

        # Call modify command
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'cpuset', 'modify',
            'test_modify_mems', '-m', '0'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0)
        self.assertIn('Memory nodes:', result.stdout)

    def test_modify_set_isolated(self):
        """Test setting partition to isolated"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_modify_isolated')
        cs.write_memnode('0')
        cs.assign_cpus('0')
        self.test_cpusets.append('test_modify_isolated')

        # Call modify command
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'cpuset', 'modify',
            'test_modify_isolated', '--isolated'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0)
        self.assertIn('Partition:', result.stdout)
        self.assertIn('isolated', result.stdout)

    def test_modify_unset_isolated(self):
        """Test setting partition to member (not isolated)"""
        # Create test cpuset with isolated partition
        cs = cpuset.Cpuset('test_modify_member')
        cs.write_memnode('0')
        cs.assign_cpus('0')
        cs.set_cpu_exclusive()
        self.test_cpusets.append('test_modify_member')

        # Call modify command to unset isolated
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'cpuset', 'modify',
            'test_modify_member', '--no-isolated'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0)
        self.assertIn('Partition:', result.stdout)
        self.assertIn('member', result.stdout)

    def test_modify_multiple_changes(self):
        """Test modifying multiple properties at once"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_modify_multi')
        cs.write_memnode('0')
        cs.assign_cpus('0-1')
        self.test_cpusets.append('test_modify_multi')

        # Call modify command with multiple changes
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'cpuset', 'modify',
            'test_modify_multi',
            '--add-cpus', '2-3',
            '-m', '0',
            '--isolated'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0)
        self.assertIn('CPUs:', result.stdout)
        self.assertIn('Memory nodes:', result.stdout)
        self.assertIn('Partition:', result.stdout)

    def test_modify_nonexistent_cpuset_error(self):
        """Test error when modifying nonexistent cpuset"""
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'cpuset', 'modify',
            'nonexistent_cpuset', '--add-cpus', '0'
        ], capture_output=True, text=True)

        self.assertNotEqual(result.returncode, 0)
        self.assertIn('does not exist', result.stderr)

    def test_modify_no_options_error(self):
        """Test error when no modification options specified"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_modify_no_opts')
        cs.write_memnode('0')
        cs.assign_cpus('0')
        self.test_cpusets.append('test_modify_no_opts')

        # Call modify with no options
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'cpuset', 'modify',
            'test_modify_no_opts'
        ], capture_output=True, text=True)

        self.assertNotEqual(result.returncode, 0)
        self.assertIn('at least one modification', result.stderr)

    def test_modify_conflicting_partition_options_error(self):
        """Test error when both --isolated and --no-isolated specified"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_modify_conflict')
        cs.write_memnode('0')
        cs.assign_cpus('0')
        self.test_cpusets.append('test_modify_conflict')

        # This should be caught by argparse, not our code
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'cpuset', 'modify',
            'test_modify_conflict', '--isolated', '--no-isolated'
        ], capture_output=True, text=True)

        self.assertNotEqual(result.returncode, 0)
        # argparse will complain about mutually exclusive group
        self.assertTrue(result.stderr)  # Should have error output


@unittest.skipUnless(os.geteuid() == 0, "Requires root permissions")
@unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
class TestTunaMoveCpusetCLI(unittest.TestCase):
    """Test 'tuna move --cpuset' command (moves entities to cpusets)"""

    def setUp(self):
        """Create test cpusets"""
        self.test_cpusets = []

    def tearDown(self):
        """Clean up test cpusets and move test process back to root"""
        # Move current process back to root
        try:
            root = cpuset.CpusetsInit()
            root.write_pid(os.getpid())
        except:
            pass

        # Clean up cpusets
        for cs_name in self.test_cpusets:
            try:
                cpuset.destroy_cpuset(cs_name, force=True)
            except:
                pass

    def test_move_threads_to_cpuset(self):
        """Test moving threads to cpuset using tuna move --cpuset"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_tuna_move')
        cs.write_memnode('0')
        cs.assign_cpus('0-1')
        self.test_cpusets.append('test_tuna_move')

        # Move current process using tuna move --cpuset
        current_pid = os.getpid()
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'move',
            '--cpuset', 'test_tuna_move', '-t', str(current_pid)
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0)
        self.assertIn('Moved 1 task(s)', result.stdout)

        # Verify PID is in the cpuset
        with open('/sys/fs/cgroup/test_tuna_move/cgroup.procs') as f:
            pids = f.read().strip().split('\n')
        self.assertIn(str(current_pid), pids)

    def test_move_threads_by_pattern_to_cpuset(self):
        """Test moving threads by pattern to cpuset"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_tuna_move_pattern')
        cs.write_memnode('0')
        cs.assign_cpus('0-1')
        self.test_cpusets.append('test_tuna_move_pattern')

        # Move current process (using PID as pattern)
        current_pid = os.getpid()
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'move',
            '--cpuset', 'test_tuna_move_pattern', '-t', str(current_pid)
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0)
        self.assertIn('Moved', result.stdout)

        # Verify it moved
        with open('/sys/fs/cgroup/test_tuna_move_pattern/cgroup.procs') as f:
            pids = f.read().strip().split('\n')
        self.assertIn(str(current_pid), pids)

    def test_move_nonexistent_cpuset_error(self):
        """Test error when moving to nonexistent cpuset"""
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'move',
            '--cpuset', 'nonexistent_cpuset', '-t', '1'
        ], capture_output=True, text=True)

        self.assertNotEqual(result.returncode, 0)
        self.assertIn('does not exist', result.stderr)

    def test_move_blocklisted_process_to_cpuset(self):
        """Test that blocklisted processes cannot be moved via tuna move --cpuset"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_blocklist_move')
        cs.write_memnode('0')
        cs.assign_cpus('0-1')
        self.test_cpusets.append('test_blocklist_move')

        # Try to move PID 1 (systemd) - should be blocked
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'move',
            '--cpuset', 'test_blocklist_move', '-t', '1'
        ], capture_output=True, text=True)

        # Should complete but with 0 successful moves
        self.assertEqual(result.returncode, 0)
        self.assertIn('0 task(s)', result.stdout)

        # Verify PID 1 is NOT in the cpuset
        with open('/sys/fs/cgroup/test_blocklist_move/cgroup.procs') as f:
            pids = f.read().strip().split('\n')
        self.assertNotIn('1', pids)

    def test_move_mixed_valid_and_invalid_pids(self):
        """Test moving mix of valid and invalid PIDs"""
        # Create test cpuset
        cs = cpuset.Cpuset('test_mixed_move')
        cs.write_memnode('0')
        cs.assign_cpus('0-1')
        self.test_cpusets.append('test_mixed_move')

        # Move our PID (valid) and PID 999999 (likely invalid)
        current_pid = os.getpid()
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'move',
            '--cpuset', 'test_mixed_move', '-t', f'{current_pid},999999'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0)
        # Should show some successes and some failures
        self.assertIn('Moved', result.stdout)
        self.assertIn('failed', result.stdout)

        # Our PID should be in the cpuset
        with open('/sys/fs/cgroup/test_mixed_move/cgroup.procs') as f:
            pids = f.read().strip().split('\n')
        self.assertIn(str(current_pid), pids)


@unittest.skipUnless(os.geteuid() == 0, "Requires root permissions")
@unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
class TestTunaIsolateCpusetCLI(unittest.TestCase):
    """Test 'tuna isolate --cpuset' command (cpuset-based CPU isolation)"""

    def setUp(self):
        """Clean up any test cpusets before each test"""
        self.test_cpusets = []
        # Clean up any existing test cpusets
        try:
            cpuset.cleanup_cpusets('tuna_*', force=True, recursive=False)
            cpuset.cleanup_cpusets('my_*', force=True, recursive=False)
            cpuset.cleanup_cpusets('test_*', force=True, recursive=False)
        except:
            pass

    def tearDown(self):
        """Clean up test cpusets after each test"""
        # Clean up by pattern (catches both named and default cpusets)
        try:
            cpuset.cleanup_cpusets('tuna_*', force=True, recursive=False)
            cpuset.cleanup_cpusets('my_*', force=True, recursive=False)
            cpuset.cleanup_cpusets('test_*', force=True, recursive=False)
        except:
            pass

    def test_isolate_simple_cpuset_default_name(self):
        """Test simple isolated cpuset with default name"""
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'isolate',
            '-c', '0-1', '--cpuset'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0, f"Command failed: {result.stderr}")
        self.assertIn('tuna_isolated', result.stdout)

        # Verify cpuset exists and has correct properties
        cpuset_path = '/sys/fs/cgroup/tuna_isolated'
        self.assertTrue(os.path.exists(cpuset_path))

        with open(f'{cpuset_path}/cpuset.cpus') as f:
            cpus = f.read().strip()
            self.assertIn('0', cpus)
            self.assertIn('1', cpus)

        with open(f'{cpuset_path}/cpuset.cpus.partition') as f:
            partition = f.read().strip()
            self.assertEqual(partition, 'isolated')

        # Verify it's empty (no tasks)
        with open(f'{cpuset_path}/cgroup.procs') as f:
            tasks = f.read().strip()
            self.assertEqual(tasks, '', 'Isolated cpuset should be empty')

    def test_isolate_custom_isolated_name(self):
        """Test isolated cpuset with custom name"""
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'isolate',
            '-c', '0-1', '--cpuset', 'my_rt'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0, f"Command failed: {result.stderr}")
        self.assertIn('my_rt', result.stdout)

        # Verify cpuset exists
        cpuset_path = '/sys/fs/cgroup/my_rt'
        self.assertTrue(os.path.exists(cpuset_path))

        with open(f'{cpuset_path}/cpuset.cpus') as f:
            cpus = f.read().strip()
            self.assertIn('0', cpus)

        with open(f'{cpuset_path}/cpuset.cpus.partition') as f:
            partition = f.read().strip()
            self.assertEqual(partition, 'isolated')

    def test_isolate_with_housekeeping_default_names(self):
        """Test isolated + housekeeping cpusets with default names"""
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'isolate',
            '-c', '0-1', '--cpuset', '--cpuset-housekeeping', '2-3'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0, f"Command failed: {result.stderr}")
        self.assertIn('tuna_isolated', result.stdout)
        self.assertIn('tuna_housekeeping', result.stdout)

        # Verify isolated cpuset
        isolated_path = '/sys/fs/cgroup/tuna_isolated'
        self.assertTrue(os.path.exists(isolated_path))

        with open(f'{isolated_path}/cpuset.cpus') as f:
            cpus = f.read().strip()
            self.assertIn('0', cpus)

        with open(f'{isolated_path}/cpuset.cpus.partition') as f:
            partition = f.read().strip()
            self.assertEqual(partition, 'isolated')

        # Verify housekeeping cpuset
        hk_path = '/sys/fs/cgroup/tuna_housekeeping'
        self.assertTrue(os.path.exists(hk_path))

        with open(f'{hk_path}/cpuset.cpus') as f:
            cpus = f.read().strip()
            self.assertIn('2', cpus)
            self.assertIn('3', cpus)

        with open(f'{hk_path}/cpuset.cpus.partition') as f:
            partition = f.read().strip()
            self.assertEqual(partition, 'member')

        # Verify processes were migrated to housekeeping
        with open(f'{hk_path}/cgroup.procs') as f:
            tasks = f.read().strip().split('\n')
            # Should have tasks migrated (at least some)
            self.assertGreater(len(tasks), 0, 'Housekeeping cpuset should have migrated tasks')
            # Filter out empty strings
            tasks = [t for t in tasks if t]
            self.assertGreater(len(tasks), 0)

    def test_isolate_custom_names_both_cpusets(self):
        """Test isolated + housekeeping with custom names for both"""
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'isolate',
            '-c', '0-1', '--cpuset', 'my_rt',
            '--cpuset-housekeeping', 'my_hk', '2-3'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0, f"Command failed: {result.stderr}")
        self.assertIn('my_rt', result.stdout)
        self.assertIn('my_hk', result.stdout)

        # Verify both cpusets exist
        self.assertTrue(os.path.exists('/sys/fs/cgroup/my_rt'))
        self.assertTrue(os.path.exists('/sys/fs/cgroup/my_hk'))

        # Verify my_rt has correct CPUs
        with open('/sys/fs/cgroup/my_rt/cpuset.cpus') as f:
            cpus = f.read().strip()
            self.assertIn('0', cpus)

        # Verify my_hk has correct CPUs
        with open('/sys/fs/cgroup/my_hk/cpuset.cpus') as f:
            cpus = f.read().strip()
            self.assertIn('2', cpus)
            self.assertIn('3', cpus)

        # Verify partition types: isolated cpuset should be 'isolated', housekeeping should be 'member'
        with open('/sys/fs/cgroup/my_rt/cpuset.cpus.partition') as f:
            self.assertEqual(f.read().strip(), 'isolated')

        with open('/sys/fs/cgroup/my_hk/cpuset.cpus.partition') as f:
            self.assertEqual(f.read().strip(), 'member')

    def test_isolate_housekeeping_isolated_flag(self):
        """Test --housekeeping-isolated flag makes housekeeping partition=isolated"""
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'isolate',
            '-c', '0-1', '--cpuset', 'test_iso',
            '--cpuset-housekeeping', 'test_hk_iso', '2-3',
            '--housekeeping-isolated'
        ], capture_output=True, text=True)

        self.assertEqual(result.returncode, 0, f"Command failed: {result.stderr}")
        self.assertIn('test_iso', result.stdout)
        self.assertIn('test_hk_iso', result.stdout)

        # Verify both cpusets exist
        self.assertTrue(os.path.exists('/sys/fs/cgroup/test_iso'))
        self.assertTrue(os.path.exists('/sys/fs/cgroup/test_hk_iso'))

        # Both should have partition=isolated when --housekeeping-isolated is used
        with open('/sys/fs/cgroup/test_iso/cpuset.cpus.partition') as f:
            self.assertEqual(f.read().strip(), 'isolated')

        with open('/sys/fs/cgroup/test_hk_iso/cpuset.cpus.partition') as f:
            self.assertEqual(f.read().strip(), 'isolated')

    def test_isolate_housekeeping_without_cpuset_error(self):
        """Test error when using --cpuset-housekeeping without --cpuset"""
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'isolate',
            '-c', '0-1', '--cpuset-housekeeping', '2-3'
        ], capture_output=True, text=True)

        self.assertNotEqual(result.returncode, 0)
        self.assertIn('requires --cpuset', result.stderr)

    def test_isolate_housekeeping_wrong_arg_count_error(self):
        """Test error when --cpuset-housekeeping has wrong number of arguments"""
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'isolate',
            '-c', '0-1', '--cpuset',
            '--cpuset-housekeeping', 'foo', 'bar', 'baz'
        ], capture_output=True, text=True)

        self.assertNotEqual(result.returncode, 0)
        self.assertIn('1 or 2 arguments', result.stderr)

    def test_isolate_traditional_without_cpuset(self):
        """Test traditional isolate without --cpuset (backward compatibility)"""
        result = subprocess.run([
            sys.executable, './tuna-cmd.py', 'isolate',
            '-c', '0-1'
        ], capture_output=True, text=True)

        # Should succeed
        self.assertEqual(result.returncode, 0)

        # Verify no cpusets were created
        self.assertFalse(os.path.exists('/sys/fs/cgroup/tuna_isolated'))
        self.assertFalse(os.path.exists('/sys/fs/cgroup/tuna_housekeeping'))


@unittest.skipUnless(os.geteuid() == 0, "Requires root permissions")
@unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
class TestTunaRunCpusetCLI(unittest.TestCase):
    """Test tuna run --cpuset command"""

    def setUp(self):
        """Clean up before each test"""
        self.test_cpuset = 'test_run_cpuset'
        try:
            cpuset.destroy_cpuset(self.test_cpuset, force=True)
        except:
            pass

    def tearDown(self):
        """Clean up after each test"""
        try:
            cpuset.destroy_cpuset(self.test_cpuset, force=True)
        except:
            pass

    def test_run_with_cpuset_basic(self):
        """Test running a command in a cpuset"""
        # Create test cpuset
        cs = cpuset.Cpuset(self.test_cpuset)
        cs.assign_cpus('0-1')
        cs.write_memnode('0')

        # Run a simple command in the cpuset
        result = subprocess.run(
            [sys.executable, 'tuna-cmd.py', 'run', '--cpuset', self.test_cpuset, 'echo hello'],
            capture_output=True,
            text=True
        )

        self.assertEqual(result.returncode, 0)
        self.assertIn('hello', result.stdout)

    def test_run_with_cpuset_and_priority(self):
        """Test running a command with both cpuset and priority"""
        # Create test cpuset
        cs = cpuset.Cpuset(self.test_cpuset)
        cs.assign_cpus('0-1')
        cs.write_memnode('0')

        # Run command with priority
        result = subprocess.run(
            [sys.executable, 'tuna-cmd.py', 'run', '--cpuset', self.test_cpuset,
             '-p', 'FIFO:50', 'echo priority test'],
            capture_output=True,
            text=True
        )

        self.assertEqual(result.returncode, 0)
        self.assertIn('priority test', result.stdout)

    def test_run_with_nonexistent_cpuset(self):
        """Test error handling for non-existent cpuset"""
        result = subprocess.run(
            [sys.executable, 'tuna-cmd.py', 'run', '--cpuset', 'nonexistent_cpuset', 'echo test'],
            capture_output=True,
            text=True
        )

        self.assertNotEqual(result.returncode, 0)
        self.assertIn('does not exist', result.stderr)

    def test_run_cpuset_mutually_exclusive_with_cpus(self):
        """Test that --cpuset and -c are mutually exclusive"""
        # Create test cpuset
        cs = cpuset.Cpuset(self.test_cpuset)
        cs.assign_cpus('0-1')
        cs.write_memnode('0')

        # Try to use both --cpuset and -c
        result = subprocess.run(
            [sys.executable, 'tuna-cmd.py', 'run', '--cpuset', self.test_cpuset,
             '-c', '0-1', 'echo test'],
            capture_output=True,
            text=True
        )

        # Should fail with error about mutually exclusive arguments
        self.assertNotEqual(result.returncode, 0)
        self.assertIn('not allowed with argument', result.stderr)

    def test_run_help_shows_cpuset_option(self):
        """Test that --cpuset option appears in help"""
        result = subprocess.run(
            [sys.executable, 'tuna-cmd.py', 'run', '--help'],
            capture_output=True,
            text=True
        )

        self.assertEqual(result.returncode, 0)
        self.assertIn('--cpuset', result.stdout)
        self.assertIn('Run process in specified cpuset', result.stdout)


def suite():
    """Create test suite"""
    loader = unittest.TestLoader()
    suite = unittest.TestSuite()

    # Add all test classes
    suite.addTests(loader.loadTestsFromTestCase(TestGetNextTunaCpusetName))
    suite.addTests(loader.loadTestsFromTestCase(TestCpusetCreateCLI))
    suite.addTests(loader.loadTestsFromTestCase(TestCpusetListCLI))
    suite.addTests(loader.loadTestsFromTestCase(TestCpusetDestroyCLI))
    suite.addTests(loader.loadTestsFromTestCase(TestCpusetMoveCLI))
    suite.addTests(loader.loadTestsFromTestCase(TestCpusetShowCLI))
    suite.addTests(loader.loadTestsFromTestCase(TestCpusetStatusCLI))
    suite.addTests(loader.loadTestsFromTestCase(TestCpusetModifyCLI))
    suite.addTests(loader.loadTestsFromTestCase(TestTunaMoveCpusetCLI))
    suite.addTests(loader.loadTestsFromTestCase(TestTunaIsolateCpusetCLI))
    suite.addTests(loader.loadTestsFromTestCase(TestTunaRunCpusetCLI))

    return suite


if __name__ == '__main__':
    # Check if running as root
    if os.geteuid() != 0:
        print("\n" + "="*60)
        print("WARNING: Not running as root")
        print("="*60)
        print("All CLI tests require root permissions.")
        print("\nTo run tests:")
        print("  sudo python3 -m unittest tests.test_cpuset_cli -v")
        print("="*60 + "\n")
        sys.exit(1)

    # Check cgroup v2 support
    ci = cpuset.CpusetsInit()
    if not ci.supported:
        print("\n" + "="*60)
        print("ERROR: cgroup v2 not supported")
        print("="*60)
        print("These tests require cgroup v2 support.")
        print("="*60 + "\n")
        sys.exit(1)

    # Run tests
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(suite())

    # Exit with appropriate code
    sys.exit(0 if result.wasSuccessful() else 1)
