from __future__ import annotations

import os
import sys
from pathlib import Path
from unittest.mock import MagicMock, mock_open, patch
import pytest

# Add src to python path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))

from actions.site import handle_site_create, handle_site_delete


@pytest.fixture
def mock_config():
    cfg = MagicMock()
    cfg.api_mock = True
    cfg.api_url = "http://localhost:8000"
    return cfg


@pytest.fixture
def mock_api_client():
    client = MagicMock()
    client.update_site_status = MagicMock()
    return client


@patch("subprocess.run")
@patch("os.makedirs")
@patch("os.symlink")
@patch("os.unlink")
@patch("os.path.lexists", return_value=False)
@patch("builtins.open", new_callable=mock_open)
@patch("actions.site.load_agent_token", return_value="test-token")
@patch("actions.site.handle_sftp_create")
@patch("actions.site.handle_database_create")
@patch("actions.site.handle_ssl_generate")
def test_site_create_static_success(
    mock_ssl, mock_db, mock_sftp, mock_token, mock_file_open,
    mock_lexists, mock_unlink, mock_symlink, mock_makedirs, mock_run,
    mock_config, mock_api_client
):
    # Mock Nginx test & systemctl to succeed
    mock_run.return_value = MagicMock(returncode=0, stdout="syntax ok", stderr="")

    payload = {
        "site_id": 42,
        "domain": "example.com",
        "www_alias": True,
        "docroot": "/var/www/example.com/public",
        "runtime": "static",
        "starter_package": {
            "sftp": True,
            "database": True,
            "ssl": True
        }
    }

    res = handle_site_create(payload, mock_config, mock_api_client)

    assert res["success"] is True
    assert res["domain"] == "example.com"

    # Verify directory creation
    mock_makedirs.assert_any_call("/var/www/example.com/public", exist_ok=True)

    # Verify chown and chmod are called on docroot parent directory
    mock_run.assert_any_call(["chown", "www-data:www-data", "/var/www/example.com"], shell=False, check=True)
    mock_run.assert_any_call(["chmod", "755", "/var/www/example.com"], shell=False, check=True)

    # Verify index.html file open write
    mock_file_open.assert_any_call(os.path.normpath("/var/www/example.com/public/index.html"), "w", encoding="utf-8")

    # Verify Nginx config write
    mock_file_open.assert_any_call("/etc/nginx/sites-available/example.com.conf", "w", encoding="utf-8")
    mock_symlink.assert_called_once_with("/etc/nginx/sites-available/example.com.conf", "/etc/nginx/sites-enabled/example.com.conf")

    # Verify starter package calls
    mock_sftp.assert_called_once_with({
        "username_suffix": "example_com",
        "password": "Wolf_example_com_2026",
        "home_directory": "/var/www/example.com/public"
    })
    mock_db.assert_called_once_with({
        "name": "example_com"
    })
    mock_ssl.assert_called_once_with({
        "domain": "example.com",
        "email": "admin@example.com",
        "provider": "letsencrypt"
    })

    # Verify backend api status call
    mock_api_client.update_site_status.assert_called_once_with(
        42, {"status": "active", "error": None}, "test-token"
    )


@patch("subprocess.run")
@patch("os.makedirs")
@patch("os.symlink")
@patch("os.unlink")
@patch("os.path.lexists", return_value=False)
@patch("builtins.open", new_callable=mock_open)
@patch("actions.site.load_agent_token", return_value="test-token")
def test_site_create_php_runtime(
    mock_token, mock_file_open, mock_lexists, mock_unlink, mock_symlink,
    mock_makedirs, mock_run, mock_config, mock_api_client
):
    # Mock Nginx test & systemctl to succeed
    mock_run.return_value = MagicMock(returncode=0, stdout="syntax ok", stderr="")

    payload = {
        "site_id": 43,
        "domain": "my-php-site.org",
        "www_alias": False,
        "docroot": "/var/www/my-php-site.org",
        "runtime": "php",
        "php_version": "8.2",
        "starter_package": {}
    }

    res = handle_site_create(payload, mock_config, mock_api_client)

    assert res["success"] is True

    # Verify FPM pool conf is written
    mock_file_open.assert_any_call("/etc/php/8.2/fpm/pool.d/my-php-site.org.conf", "w", encoding="utf-8")
    # Verify PHP-FPM reload command run
    mock_run.assert_any_call(["systemctl", "reload", "php8.2-fpm"], shell=False, check=True)


@patch("subprocess.run")
@patch("os.makedirs")
@patch("os.symlink")
@patch("os.unlink")
@patch("os.path.lexists", return_value=False)
@patch("builtins.open", new_callable=mock_open)
@patch("actions.site.load_agent_token", return_value="test-token")
def test_site_create_php_fpm_reload_failure_soft_fails(
    mock_token, mock_file_open, mock_lexists, mock_unlink, mock_symlink,
    mock_makedirs, mock_run, mock_config, mock_api_client
):
    # Mock subprocess.run to raise exception on PHP-FPM reload, but succeed on Nginx
    def run_side_effect(cmd, *args, **kwargs):
        if "php8.2-fpm" in cmd:
            raise subprocess.CalledProcessError(1, cmd, stderr="reload failed")
        return MagicMock(returncode=0, stdout="", stderr="")

    mock_run.side_effect = run_side_effect

    payload = {
        "site_id": 44,
        "domain": "php-fail.com",
        "www_alias": False,
        "docroot": "/var/www/php-fail.com",
        "runtime": "php",
        "php_version": "8.2"
    }

    res = handle_site_create(payload, mock_config, mock_api_client)

    # Should still succeed because PHP-FPM reload is a soft fail
    assert res["success"] is True
    # Should update backend with active status
    mock_api_client.update_site_status.assert_called_with(
        44, {"status": "active", "error": None}, "test-token"
    )


@patch("subprocess.run")
@patch("os.makedirs")
@patch("os.symlink")
@patch("os.unlink")
@patch("os.path.lexists", return_value=True)
@patch("builtins.open", new_callable=mock_open)
@patch("actions.site.load_agent_token", return_value="test-token")
def test_site_create_nginx_test_failure_raises_error(
    mock_token, mock_file_open, mock_lexists, mock_unlink, mock_symlink,
    mock_makedirs, mock_run, mock_config, mock_api_client
):
    # Mock Nginx test to fail
    mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="nginx emerg syntax error")

    payload = {
        "site_id": 45,
        "domain": "bad-nginx.com",
        "www_alias": True,
        "docroot": "/var/www/bad-nginx.com",
        "runtime": "static"
    }

    res = handle_site_create(payload, mock_config, mock_api_client)

    # Critical Step failed -> returns success=False
    assert res["success"] is False
    assert "Nginx configuration test failed" in res["error"]

    # Symlink is unlinked on test failure
    mock_unlink.assert_any_call("/etc/nginx/sites-enabled/bad-nginx.com.conf")

    # Backend is notified with failed status
    mock_api_client.update_site_status.assert_called_once_with(
        45, {"status": "failed", "error": res["error"]}, "test-token"
    )


@patch("shutil.which", return_value=None)
@patch("os.makedirs")
@patch("actions.site.load_agent_token", return_value="test-token")
def test_site_create_nginx_missing_fails_fast_no_side_effects(
    mock_token, mock_makedirs, mock_which, mock_config, mock_api_client
):
    mock_config.server_id = "srv-123"

    payload = {
        "site_id": 50,
        "domain": "no-nginx.com",
        "docroot": "/var/www/no-nginx.com",
        "runtime": "static",
    }

    res = handle_site_create(payload, mock_config, mock_api_client)

    assert res["success"] is False
    assert "nginx binary not found" in res["error"]
    assert res["error"].startswith("[server_id=srv-123]")

    # No filesystem side effects when nginx isn't present at all.
    mock_makedirs.assert_not_called()

    mock_api_client.update_site_status.assert_called_once_with(
        50, {"status": "failed", "error": res["error"]}, "test-token"
    )


@patch("shutil.which", return_value="/usr/sbin/nginx")
@patch("shutil.rmtree")
@patch("subprocess.run")
@patch("os.makedirs")
@patch("os.symlink")
@patch("os.unlink")
@patch("os.remove")
@patch("os.path.exists", return_value=True)
@patch("os.path.isdir", return_value=False)
@patch("os.path.lexists", return_value=True)
@patch("builtins.open", new_callable=mock_open)
@patch("actions.site.load_agent_token", return_value="test-token")
def test_site_create_reload_failure_rolls_back_created_files(
    mock_token, mock_file_open, mock_lexists, mock_isdir, mock_exists,
    mock_remove, mock_unlink, mock_symlink, mock_makedirs, mock_run,
    mock_rmtree, mock_which, mock_config, mock_api_client
):
    # nginx -t succeeds, but the final `systemctl reload nginx` fails.
    import subprocess as sp

    def run_side_effect(cmd, *args, **kwargs):
        if cmd[:2] == ["nginx", "-t"]:
            return MagicMock(returncode=0, stdout="syntax ok", stderr="")
        if cmd == ["systemctl", "reload", "nginx"]:
            raise sp.CalledProcessError(1, cmd, stderr="reload failed")
        return MagicMock(returncode=0, stdout="", stderr="")

    mock_run.side_effect = run_side_effect
    mock_config.server_id = "srv-9"

    payload = {
        "site_id": 51,
        "domain": "reload-fail.com",
        "docroot": "/var/www/reload-fail.com",
        "runtime": "static",
    }

    res = handle_site_create(payload, mock_config, mock_api_client)

    assert res["success"] is False
    assert res["error"].startswith("[server_id=srv-9]")

    # Since domain_root did not pre-exist, the freshly created site root
    # (config + docroot) is rolled back in full.
    mock_remove.assert_any_call("/etc/nginx/sites-available/reload-fail.com.conf")
    mock_unlink.assert_any_call("/etc/nginx/sites-enabled/reload-fail.com.conf")
    mock_rmtree.assert_called_once_with("/var/www/reload-fail.com", ignore_errors=True)

    mock_api_client.update_site_status.assert_called_once_with(
        51, {"status": "failed", "error": res["error"]}, "test-token"
    )


@patch("shutil.which", return_value="/usr/sbin/nginx")
@patch("shutil.rmtree")
@patch("subprocess.run")
@patch("os.makedirs")
@patch("os.symlink")
@patch("os.unlink")
@patch("os.remove")
@patch("os.path.exists", return_value=True)
@patch("os.path.isdir", return_value=True)
@patch("os.path.lexists", return_value=False)
@patch("builtins.open", new_callable=mock_open)
@patch("actions.site.load_agent_token", return_value="test-token")
def test_site_create_rollback_never_deletes_preexisting_docroot(
    mock_token, mock_file_open, mock_lexists, mock_isdir, mock_exists,
    mock_remove, mock_unlink, mock_symlink, mock_makedirs, mock_run,
    mock_rmtree, mock_which, mock_config, mock_api_client
):
    # domain_root already existed before this attempt (os.path.isdir -> True):
    # rollback must never rmtree it, even though provisioning still fails.
    mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="syntax error")

    payload = {
        "site_id": 52,
        "domain": "preexisting.com",
        "docroot": "/var/www/preexisting.com",
        "runtime": "static",
    }

    res = handle_site_create(payload, mock_config, mock_api_client)

    assert res["success"] is False
    mock_rmtree.assert_not_called()


def test_site_create_validation_blocks_path_traversal(mock_config, mock_api_client):
    payloads = [
        {"domain": "../outside", "docroot": "/var/www/outside", "site_id": 1},
        {"domain": "site.com", "docroot": "/etc/nginx", "site_id": 1},
        {"domain": "site.com", "docroot": "/var/www/../../etc/nginx", "site_id": 1},
        {"domain": "site.com", "docroot": "d:/programs/var/www/site", "site_id": 1} # Outside linux /var/www structure
    ]

    for p in payloads:
        res = handle_site_create(p, mock_config, mock_api_client)
        assert res["success"] is False
        assert ("Invalid domain" in res["error"] or "must be under /var/www" in res["error"])


@patch("os.path.exists", return_value=True)
@patch("os.path.lexists", return_value=True)
@patch("subprocess.run")
@patch("os.unlink")
@patch("os.remove")
@patch("glob.glob", return_value=["/etc/php/8.2/fpm/pool.d/del.com.conf"])
@patch("shutil.rmtree")
def test_site_delete_success(
    mock_rmtree, mock_glob, mock_remove, mock_unlink, mock_run,
    mock_lexists, mock_exists, mock_config, mock_api_client
):
    mock_run.return_value = MagicMock(returncode=0)

    payload = {
        "domain": "del.com",
        "docroot": "/var/www/del.com"
    }

    res = handle_site_delete(payload, mock_config, mock_api_client)

    assert res["success"] is True

    # Check Nginx vhost deletion files
    mock_unlink.assert_called_once_with("/etc/nginx/sites-enabled/del.com.conf")
    mock_remove.assert_any_call("/etc/nginx/sites-available/del.com.conf")

    # Check PHP FPM pool deletion
    mock_remove.assert_any_call("/etc/php/8.2/fpm/pool.d/del.com.conf")

    # Check docroot removal
    mock_rmtree.assert_called_once_with("/var/www/del.com", ignore_errors=True)

    # Check nginx service reload was run
    mock_run.assert_called_once_with(["systemctl", "reload", "nginx"], shell=False, check=True)


@patch("shutil.rmtree")
def test_site_delete_security_restrictions(mock_rmtree, mock_config, mock_api_client):
    invalid_payloads = [
        {"domain": "../etc", "docroot": "/var/www/del.com"},
        {"domain": "del.com", "docroot": "/var/www"},
        {"domain": "del.com", "docroot": "/var/www/"},
        {"domain": "del.com", "docroot": "/etc/nginx"},
    ]

    for p in invalid_payloads:
        res = handle_site_delete(p, mock_config, mock_api_client)
        assert res["success"] is False
        assert ("Invalid domain" in res["error"] or "must be strictly under /var/www" in res["error"])

    mock_rmtree.assert_not_called()
