#!/usr/bin/env python3
"""Tests for the tunectl Python CLI wrapper.

All tests use subprocess to invoke the CLI, matching how users run it.
Tests verify: exit codes, stdout/stderr content, subcommand behavior.

Run with: python3 tests/test_cli.py
    or:   python3 -m pytest tests/test_cli.py -v
"""

import json
import os
import subprocess
import sys
import unittest

# Project root: one level above the tests/ directory
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# We run the CLI via `python3 -m tunectl` from the project root
CLI_CMD = [sys.executable, "-m", "tunectl"]


def run_cli(*args: str, timeout: int = 30) -> subprocess.CompletedProcess:
    """Run the tunectl CLI with the given arguments."""
    cmd = CLI_CMD + list(args)
    return subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        cwd=PROJECT_ROOT,
        timeout=timeout,
    )


class TestHelpOutput(unittest.TestCase):
    """Tests for --help on root command and subcommands."""

    def test_root_help_shows_all_subcommands(self):
        """VAL-CLI-011: --help on root shows all 6 subcommands."""
        result = run_cli("--help")
        self.assertEqual(result.returncode, 0)
        stdout = result.stdout
        for cmd in ("discover", "plan", "apply", "rollback", "audit", "benchmark"):
            self.assertIn(cmd, stdout, f"Missing subcommand '{cmd}' in --help output")

    def test_root_help_shows_descriptions(self):
        """--help shows descriptions for each subcommand."""
        result = run_cli("--help")
        self.assertEqual(result.returncode, 0)
        # Check for key description fragments
        self.assertIn("Detect system environment", result.stdout)
        self.assertIn("dry-run tuning plan", result.stdout)
        self.assertIn("Apply tuning changes", result.stdout)
        self.assertIn("Restore system config", result.stdout)
        self.assertIn("Verify applied tuning", result.stdout)
        self.assertIn("performance benchmarks", result.stdout)

    def test_no_subcommand_shows_help_and_exits_2(self):
        """Running with no args shows help and exits 2."""
        result = run_cli()
        self.assertEqual(result.returncode, 2)

    def test_discover_help(self):
        """discover --help shows usage and exits 0."""
        result = run_cli("discover", "--help")
        self.assertEqual(result.returncode, 0)
        self.assertIn("discover", result.stdout)

    def test_plan_help(self):
        """plan --help shows --tier option and exits 0."""
        result = run_cli("plan", "--help")
        self.assertEqual(result.returncode, 0)
        self.assertIn("--tier", result.stdout)

    def test_apply_help(self):
        """apply --help shows --tier option and exits 0."""
        result = run_cli("apply", "--help")
        self.assertEqual(result.returncode, 0)
        self.assertIn("--tier", result.stdout)

    def test_rollback_help(self):
        """rollback --help shows --list and --backup options and exits 0."""
        result = run_cli("rollback", "--help")
        self.assertEqual(result.returncode, 0)
        self.assertIn("--list", result.stdout)
        self.assertIn("--backup", result.stdout)

    def test_audit_help(self):
        """audit --help shows usage and exits 0."""
        result = run_cli("audit", "--help")
        self.assertEqual(result.returncode, 0)
        self.assertIn("audit", result.stdout.lower())

    def test_benchmark_help(self):
        """benchmark --help shows --baseline and --compare options and exits 0."""
        result = run_cli("benchmark", "--help")
        self.assertEqual(result.returncode, 0)
        self.assertIn("--baseline", result.stdout)
        self.assertIn("--compare", result.stdout)

    def test_version(self):
        """--version shows the version string."""
        result = run_cli("--version")
        self.assertEqual(result.returncode, 0)
        self.assertIn("1.0.0", result.stdout)


class TestDiscover(unittest.TestCase):
    """Tests for the discover subcommand."""

    def test_discover_runs_and_shows_formatted_output(self):
        """VAL-CLI-001: tunectl discover runs and shows formatted summary."""
        result = run_cli("discover")
        self.assertEqual(result.returncode, 0)
        stdout = result.stdout
        # Output should NOT be raw JSON (should not start with '{')
        self.assertFalse(stdout.lstrip().startswith("{"),
                         "discover should show formatted output, not raw JSON")
        # Must contain key system fields in human-readable form
        self.assertIn("RAM", stdout)
        self.assertIn("Kernel", stdout)
        self.assertIn("CPUs", stdout)
        self.assertIn("Disk type", stdout)
        self.assertIn("OS", stdout)

    def test_discover_contains_system_values(self):
        """Formatted output contains actual system values (non-empty)."""
        result = run_cli("discover")
        self.assertEqual(result.returncode, 0)
        stdout = result.stdout
        # RAM should be a positive number (e.g. "7941 MB")
        self.assertRegex(stdout, r"\d+ MB")
        # CPU count should be a positive number
        self.assertRegex(stdout, r"CPUs.*\d+")
        # Kernel version should contain something like 6.x
        self.assertRegex(stdout, r"Kernel.*\d+\.\d+")

    def test_discover_shows_swap_status(self):
        """Formatted output includes swap status section."""
        result = run_cli("discover")
        self.assertEqual(result.returncode, 0)
        self.assertIn("Swap", result.stdout)
        self.assertIn("Configured", result.stdout)

    def test_discover_shows_sysctl_values(self):
        """Formatted output includes key sysctl values section."""
        result = run_cli("discover")
        self.assertEqual(result.returncode, 0)
        self.assertIn("sysctl", result.stdout)
        self.assertIn("vm.swappiness", result.stdout)

    def test_discover_json_flag_outputs_raw_json(self):
        """--json flag outputs raw JSON instead of formatted summary."""
        result = run_cli("discover", "--json")
        self.assertEqual(result.returncode, 0)
        # Must be valid JSON
        data = json.loads(result.stdout)
        # Must contain key system fields
        self.assertIn("ram_mb", data)
        self.assertIn("cpu_count", data)
        self.assertIn("kernel_version", data)
        self.assertIn("disk_type", data)
        self.assertIn("os_version", data)

    def test_discover_works_without_root(self):
        """VAL-CLI-019: discover works without root."""
        result = run_cli("discover")
        self.assertEqual(result.returncode, 0)


class TestPlan(unittest.TestCase):
    """Tests for the plan subcommand."""

    def test_plan_balanced_shows_entries(self):
        """VAL-CLI-002: tunectl plan --tier balanced shows dry-run plan."""
        result = run_cli("plan", "--tier", "balanced")
        self.assertEqual(result.returncode, 0)
        stdout = result.stdout
        # Should mention the tier and dry-run
        self.assertIn("balanced", stdout.lower())
        # Should list entries (look for entry IDs)
        self.assertIn("Entries to apply: 80", stdout)

    def test_plan_conservative(self):
        """Plan conservative shows 51 entries."""
        result = run_cli("plan", "--tier", "conservative")
        self.assertEqual(result.returncode, 0)
        self.assertIn("Entries to apply: 51", result.stdout)

    def test_plan_aggressive(self):
        """Plan aggressive shows 86 entries."""
        result = run_cli("plan", "--tier", "aggressive")
        self.assertEqual(result.returncode, 0)
        self.assertIn("Entries to apply: 86", result.stdout)

    def test_plan_invalid_tier_rejected(self):
        """VAL-CLI-020: Invalid tier produces error listing valid tiers."""
        result = run_cli("plan", "--tier", "extreme")
        self.assertNotEqual(result.returncode, 0)
        stderr = result.stderr
        self.assertIn("conservative", stderr)
        self.assertIn("balanced", stderr)
        self.assertIn("aggressive", stderr)

    def test_plan_missing_tier_rejected(self):
        """Missing --tier argument produces usage error."""
        result = run_cli("plan")
        self.assertNotEqual(result.returncode, 0)

    def test_plan_works_without_root(self):
        """VAL-CLI-019: plan works without root."""
        result = run_cli("plan", "--tier", "balanced")
        self.assertEqual(result.returncode, 0)


class TestApply(unittest.TestCase):
    """Tests for the apply subcommand."""

    def test_apply_invalid_tier_rejected(self):
        """VAL-CLI-020: Invalid tier in apply produces error listing valid tiers."""
        result = run_cli("apply", "--tier", "extreme")
        self.assertNotEqual(result.returncode, 0)
        stderr = result.stderr
        self.assertIn("conservative", stderr)
        self.assertIn("balanced", stderr)
        self.assertIn("aggressive", stderr)

    def test_apply_without_root_refused(self):
        """Apply without root exits non-zero (script checks root)."""
        # Skip if actually running as root (unlikely in test env)
        if os.geteuid() == 0:
            self.skipTest("Running as root — cannot test root refusal")
        result = run_cli("apply", "--tier", "conservative")
        self.assertNotEqual(result.returncode, 0)

    def test_apply_missing_tier_rejected(self):
        """Missing --tier argument produces usage error."""
        result = run_cli("apply")
        self.assertNotEqual(result.returncode, 0)


class TestRollback(unittest.TestCase):
    """Tests for the rollback subcommand."""

    def test_rollback_list(self):
        """rollback --list works (may show no backups, but should not error)."""
        result = run_cli("rollback", "--list")
        # --list is read-only and should run successfully
        # The exit code may be non-zero if no backups exist, but it should
        # not crash with a Python error
        self.assertNotIn("Traceback", result.stderr)

    def test_rollback_without_root_refused(self):
        """Rollback without root exits non-zero."""
        if os.geteuid() == 0:
            self.skipTest("Running as root — cannot test root refusal")
        result = run_cli("rollback")
        self.assertNotEqual(result.returncode, 0)


class TestAudit(unittest.TestCase):
    """Tests for the audit subcommand."""

    def test_audit_runs(self):
        """VAL-CLI-008: tunectl audit runs and shows per-check results."""
        result = run_cli("audit")
        stdout = result.stdout
        # Should contain check results
        self.assertRegex(stdout, r"\[.+\] (PASS|FAIL|SKIP)")
        # Should contain summary line
        self.assertRegex(stdout, r"\d+/\d+ checks passed")

    def test_audit_works_without_root(self):
        """VAL-CLI-019: audit works without root."""
        result = run_cli("audit")
        # Audit should run (may exit 0 or 1 depending on system state,
        # but should NOT exit 2 which would be a usage/permission error)
        self.assertNotEqual(result.returncode, 2)
        self.assertNotIn("Traceback", result.stderr)


class TestExitCodes(unittest.TestCase):
    """Tests for proper exit code propagation."""

    def test_success_exit_code_0(self):
        """VAL-CLI-015: Successful command returns exit code 0."""
        result = run_cli("discover")
        self.assertEqual(result.returncode, 0)

    def test_usage_error_exit_code_2(self):
        """VAL-CLI-015: Usage error returns exit code 2."""
        result = run_cli("plan", "--tier", "invalid_tier")
        self.assertEqual(result.returncode, 2)

    def test_script_exit_code_propagated(self):
        """VAL-CLI-015 / VAL-CROSS-011: Script exit codes propagate through CLI."""
        # discover should always succeed on this system
        result = run_cli("discover")
        self.assertEqual(result.returncode, 0)

    def test_no_command_exits_2(self):
        """No subcommand exits 2."""
        result = run_cli()
        self.assertEqual(result.returncode, 2)


class TestScriptDelegation(unittest.TestCase):
    """Tests that CLI delegates to the correct bash scripts."""

    def test_discover_delegates_to_discover_sh(self):
        """VAL-CROSS-008: discover invokes discover.sh."""
        result = run_cli("discover", "--json")
        # discover.sh outputs JSON — if we get valid JSON via --json, it ran the right script
        self.assertEqual(result.returncode, 0)
        data = json.loads(result.stdout)
        self.assertIn("ram_mb", data)

    def test_plan_delegates_to_tune_sh(self):
        """VAL-CROSS-008: plan invokes tune.sh with --dry-run."""
        result = run_cli("plan", "--tier", "balanced")
        self.assertEqual(result.returncode, 0)
        # tune.sh dry-run shows the plan header
        self.assertIn("DRY RUN", result.stdout)

    def test_audit_delegates_to_audit_sh(self):
        """VAL-CROSS-008: audit invokes audit.sh."""
        result = run_cli("audit")
        # audit.sh outputs check lines
        self.assertRegex(result.stdout, r"\[.+\] (PASS|FAIL|SKIP)")


class TestInstalledPackageSmoke(unittest.TestCase):
    """Smoke tests for pip-installed package running from outside the repo."""

    def test_pip_install_discover_works_outside_repo(self):
        """pip install + python3 -m tunectl discover works from /tmp."""
        import tempfile
        import shutil

        tmpdir = tempfile.mkdtemp(prefix="tunectl_install_test_")
        try:
            # Install the package into a temporary target directory
            install = subprocess.run(
                [sys.executable, "-m", "pip", "install", "--quiet",
                 "--target", tmpdir, PROJECT_ROOT],
                capture_output=True, text=True, timeout=60,
            )
            self.assertEqual(
                install.returncode, 0,
                f"pip install failed: {install.stderr}",
            )

            # Run discover --json from /tmp with only the installed package in path
            env = os.environ.copy()
            env["PYTHONPATH"] = tmpdir
            result = subprocess.run(
                [sys.executable, "-m", "tunectl", "discover", "--json"],
                capture_output=True, text=True,
                cwd="/tmp", env=env, timeout=30,
            )
            self.assertEqual(
                result.returncode, 0,
                f"discover failed: {result.stderr}",
            )
            data = json.loads(result.stdout)
            self.assertIn("ram_mb", data)
            self.assertIn("cpu_count", data)
        finally:
            shutil.rmtree(tmpdir, ignore_errors=True)

    def test_pip_install_plan_works_outside_repo(self):
        """pip install + python3 -m tunectl plan --tier balanced from /tmp."""
        import tempfile
        import shutil

        tmpdir = tempfile.mkdtemp(prefix="tunectl_install_test_")
        try:
            install = subprocess.run(
                [sys.executable, "-m", "pip", "install", "--quiet",
                 "--target", tmpdir, PROJECT_ROOT],
                capture_output=True, text=True, timeout=60,
            )
            self.assertEqual(
                install.returncode, 0,
                f"pip install failed: {install.stderr}",
            )

            env = os.environ.copy()
            env["PYTHONPATH"] = tmpdir
            result = subprocess.run(
                [sys.executable, "-m", "tunectl", "plan", "--tier", "balanced"],
                capture_output=True, text=True,
                cwd="/tmp", env=env, timeout=30,
            )
            self.assertEqual(
                result.returncode, 0,
                f"plan failed: {result.stderr}",
            )
            self.assertIn("Entries to apply: 80", result.stdout)
        finally:
            shutil.rmtree(tmpdir, ignore_errors=True)

    def test_pip_install_help_works_outside_repo(self):
        """pip install + python3 -m tunectl --help from /tmp."""
        import tempfile
        import shutil

        tmpdir = tempfile.mkdtemp(prefix="tunectl_install_test_")
        try:
            install = subprocess.run(
                [sys.executable, "-m", "pip", "install", "--quiet",
                 "--target", tmpdir, PROJECT_ROOT],
                capture_output=True, text=True, timeout=60,
            )
            self.assertEqual(
                install.returncode, 0,
                f"pip install failed: {install.stderr}",
            )

            env = os.environ.copy()
            env["PYTHONPATH"] = tmpdir
            result = subprocess.run(
                [sys.executable, "-m", "tunectl", "--help"],
                capture_output=True, text=True,
                cwd="/tmp", env=env, timeout=30,
            )
            self.assertEqual(result.returncode, 0)
            for cmd in ("discover", "plan", "apply", "rollback", "audit", "benchmark"):
                self.assertIn(cmd, result.stdout)
        finally:
            shutil.rmtree(tmpdir, ignore_errors=True)


# -------------------------------------------------------
# Main runner for test_cli.py standalone execution
# -------------------------------------------------------

def _run_tests():
    """Run all tests and return True if all passed."""
    print("=== test_cli.py ===")

    # Collect and run tests
    loader = unittest.TestLoader()
    suite = loader.loadTestsFromModule(sys.modules[__name__])

    # Use a verbose runner
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(suite)

    total = result.testsRun
    failures = len(result.failures) + len(result.errors)
    passed = total - failures

    print(f"\n=== test_cli.py: {passed}/{total} tests passed ===")
    return result.wasSuccessful()


if __name__ == "__main__":
    success = _run_tests()
    sys.exit(0 if success else 1)
