<?php
/**
 * Security Manager Unit Tests
 *
 * Comprehensive test suite for Care API Security Manager with focus on:
 * - Endpoint permissions validation
 * - Rate limiting enforcement
 * - Authentication requirement checks
 * - SQL injection protection
 * - XSS prevention
 *
 * @package Care_API\Tests\Unit\Security
 * @author  Descomplicar® <dev@descomplicar.pt>
 * @since   1.0.0
 */

declare(strict_types=1);

namespace Care_API\Tests\Unit\Security;

use Care_API\Security\Security_Manager;
use WP_REST_Request;
use WP_Error;

/**
 * SecurityManagerTest Class
 *
 * Unit tests for Security_Manager functionality ensuring robust security controls
 * and compliance with healthcare data protection requirements.
 */
class SecurityManagerTest extends \Care_API_Test_Case {

    /**
     * Original $_SERVER superglobal backup
     *
     * @var array
     */
    private $server_backup;

    /**
     * Test transient storage
     *
     * @var array
     */
    private $test_transients = [];

    /**
     * Set up test environment before each test
     *
     * @return void
     */
    public function setUp(): void {
        parent::setUp();

        // Backup original $_SERVER
        $this->server_backup = $_SERVER;

        // Clear test transients
        $this->test_transients = [];

        // Setup mock transient functions
        $this->setup_transient_filters();

        // Clear WordPress transients
        wp_cache_flush();
    }

    /**
     * Clean up test environment after each test
     *
     * @return void
     */
    public function tearDown(): void {
        // Restore original $_SERVER
        $_SERVER = $this->server_backup;

        // Clear test transients
        $this->test_transients = [];

        // Remove transient filters
        $this->remove_transient_filters();

        // Clear WordPress transients
        wp_cache_flush();

        parent::tearDown();
    }

    /**
     * Test 1: Validate endpoint permissions functionality
     *
     * Tests permission validation for different endpoint types:
     * - Public endpoints (status, health, version)
     * - Auth endpoints (login, password reset)
     * - Protected endpoints (require JWT)
     *
     * @covers Security_Manager::check_api_permissions
     * @covers Security_Manager::is_public_endpoint
     * @covers Security_Manager::is_auth_endpoint
     * @covers Security_Manager::validate_public_access
     * @covers Security_Manager::validate_auth_access
     * @covers Security_Manager::verify_jwt_authentication
     */
    public function test_validate_endpoint_permissions(): void {
        // Test Case 1: Invalid request object should return WP_Error
        $result = Security_Manager::check_api_permissions(null);
        $this->assertInstanceOf(WP_Error::class, $result);
        $this->assertEquals('invalid_request', $result->get_error_code());
        $this->assertEquals(400, $result->get_error_data()['status']);

        // Test Case 2: Public endpoint should be allowed with rate limiting
        $public_request = $this->create_mock_request('GET', '/kivicare/v1/status');
        $this->setup_server_vars('127.0.0.1', 'Mozilla/5.0 Test');

        // Set rate limit to pass (0 current requests)
        $this->set_test_transient('care_api_rate_limit_public_127.0.0.1', 0);

        $result = Security_Manager::check_api_permissions($public_request);
        $this->assertTrue($result);

        // Test Case 3: Auth endpoint with valid nonce should pass
        $auth_request = $this->create_mock_request('POST', '/kivicare/v1/auth/login');
        $auth_request->set_header('X-WP-Nonce', 'valid_nonce');
        $auth_request->set_header('Content-Type', 'application/json');

        // Create nonce that will validate
        $nonce = wp_create_nonce('wp_rest');
        $auth_request->set_header('X-WP-Nonce', $nonce);

        // Set rate limit to pass
        $this->set_test_transient('care_api_rate_limit_auth_127.0.0.1', 5);

        $result = Security_Manager::check_api_permissions($auth_request);
        $this->assertTrue($result);

        // Test Case 4: Protected endpoint without JWT should fail
        $unauth_request = $this->create_mock_request('GET', '/kivicare/v1/patients');

        $result = Security_Manager::check_api_permissions($unauth_request);
        $this->assertInstanceOf(WP_Error::class, $result);
        $this->assertEquals('missing_authorization', $result->get_error_code());
        $this->assertEquals(401, $result->get_error_data()['status']);

        // Test Case 5: Protected endpoint with invalid Bearer format should fail
        $invalid_auth_request = $this->create_mock_request('GET', '/kivicare/v1/patients');
        $invalid_auth_request->set_header('Authorization', 'Basic dXNlcjpwYXNz');

        $result = Security_Manager::check_api_permissions($invalid_auth_request);
        $this->assertInstanceOf(WP_Error::class, $result);
        $this->assertEquals('invalid_authorization_format', $result->get_error_code());
        $this->assertEquals(401, $result->get_error_data()['status']);
    }

    /**
     * Test 2: Rate limiting enforcement
     *
     * Tests rate limiting functionality for different endpoint types:
     * - Public endpoints: 100 requests/hour
     * - Auth endpoints: 10 requests/hour
     * - Protected endpoints: 1000 requests/hour
     *
     * @covers Security_Manager::check_rate_limit
     * @covers Security_Manager::get_client_ip
     */
    public function test_rate_limiting_enforcement(): void {
        $this->setup_server_vars('192.168.1.100', 'Mozilla/5.0 Test');

        // Test Case 1: Public endpoint within rate limit
        $public_request = $this->create_mock_request('GET', '/kivicare/v1/status');

        // Set 50 current requests, limit is 100
        $this->set_test_transient('care_api_rate_limit_public_192.168.1.100', 50);

        $result = Security_Manager::check_api_permissions($public_request);
        $this->assertTrue($result);

        // Test Case 2: Public endpoint exceeding rate limit
        $public_request_exceeded = $this->create_mock_request('GET', '/kivicare/v1/health');

        // Set 100 current requests, limit is 100
        $this->set_test_transient('care_api_rate_limit_public_192.168.1.100', 100);

        $result = Security_Manager::check_api_permissions($public_request_exceeded);
        $this->assertInstanceOf(WP_Error::class, $result);
        $this->assertEquals('rate_limit_exceeded', $result->get_error_code());
        $this->assertEquals(429, $result->get_error_data()['status']);

        // Test Case 3: Auth endpoint within strict rate limit
        $auth_request = $this->create_mock_request('POST', '/kivicare/v1/auth/login');
        $auth_request->set_header('Content-Type', 'application/json');

        // Create valid nonce
        $nonce = wp_create_nonce('wp_rest');
        $auth_request->set_header('X-WP-Nonce', $nonce);

        // Set 5 current requests, limit is 10
        $this->set_test_transient('care_api_rate_limit_auth_192.168.1.100', 5);

        $result = Security_Manager::check_api_permissions($auth_request);
        $this->assertTrue($result);

        // Test Case 4: Auth endpoint exceeding strict rate limit
        $auth_request_exceeded = $this->create_mock_request('POST', '/kivicare/v1/auth/forgot-password');

        // Set 10 current requests, limit is 10
        $this->set_test_transient('care_api_rate_limit_auth_192.168.1.100', 10);

        $result = Security_Manager::check_api_permissions($auth_request_exceeded);
        $this->assertInstanceOf(WP_Error::class, $result);
        $this->assertEquals('rate_limit_exceeded', $result->get_error_code());

        // Test Case 5: Different IP addresses have separate rate limits
        $this->setup_server_vars('10.0.0.1', 'Mozilla/5.0 Test');

        $different_ip_request = $this->create_mock_request('GET', '/kivicare/v1/status');

        // Set 0 current requests for new IP
        $this->set_test_transient('care_api_rate_limit_public_10.0.0.1', 0);

        $result = Security_Manager::check_api_permissions($different_ip_request);
        $this->assertTrue($result);
    }

    /**
     * Test 3: Authentication requirement check
     *
     * Tests authentication requirement validation for different scenarios:
     * - Missing Authorization header
     * - Invalid Bearer format
     * - JWT service availability check
     *
     * @covers Security_Manager::verify_jwt_authentication
     */
    public function test_authentication_requirement_check(): void {
        // Test Case 1: Missing Authorization header
        $request_no_auth = $this->create_mock_request('GET', '/kivicare/v1/patients');

        $result = Security_Manager::check_api_permissions($request_no_auth);
        $this->assertInstanceOf(WP_Error::class, $result);
        $this->assertEquals('missing_authorization', $result->get_error_code());
        $this->assertEquals(401, $result->get_error_data()['status']);

        // Test Case 2: Invalid Bearer format
        $request_invalid_format = $this->create_mock_request('GET', '/kivicare/v1/patients');
        $request_invalid_format->set_header('Authorization', 'Basic dXNlcjpwYXNz');

        $result = Security_Manager::check_api_permissions($request_invalid_format);
        $this->assertInstanceOf(WP_Error::class, $result);
        $this->assertEquals('invalid_authorization_format', $result->get_error_code());
        $this->assertEquals(401, $result->get_error_data()['status']);

        // Test Case 3: Valid Bearer format - JWT service will be checked
        $request_valid_format = $this->create_mock_request('GET', '/kivicare/v1/patients');
        $request_valid_format->set_header('Authorization', 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9');

        $result = Security_Manager::check_api_permissions($request_valid_format);

        // Should either succeed (if JWT service available) or fail with JWT service unavailable
        if (is_wp_error($result)) {
            $this->assertContains($result->get_error_code(), ['jwt_service_unavailable', 'invalid_token']);
        } else {
            $this->assertTrue($result);
        }

        // Test Case 4: Empty Bearer token
        $request_empty_token = $this->create_mock_request('GET', '/kivicare/v1/patients');
        $request_empty_token->set_header('Authorization', 'Bearer ');

        $result = Security_Manager::check_api_permissions($request_empty_token);

        // Should fail with JWT service unavailable or invalid token
        $this->assertInstanceOf(WP_Error::class, $result);
        $this->assertContains($result->get_error_code(), ['jwt_service_unavailable', 'invalid_token']);

        // Test Case 5: Case insensitive Bearer check
        $request_lowercase = $this->create_mock_request('GET', '/kivicare/v1/patients');
        $request_lowercase->set_header('Authorization', 'bearer valid.token');

        $result = Security_Manager::check_api_permissions($request_lowercase);

        // Should process the token (case insensitive regex)
        if (is_wp_error($result)) {
            $this->assertContains($result->get_error_code(), ['jwt_service_unavailable', 'invalid_token']);
        } else {
            $this->assertTrue($result);
        }
    }

    /**
     * Test 4: SQL injection protection
     *
     * Tests input validation and sanitization against SQL injection attacks
     * through the validate_input method with various malicious payloads.
     *
     * @covers Security_Manager::validate_input
     */
    public function test_sql_injection_protection(): void {
        // Test Case 1: SQL injection in text input
        $sql_injection_text = "'; DROP TABLE users; --";
        $result = Security_Manager::validate_input($sql_injection_text, 'text');

        // Should be sanitized (WordPress sanitize_text_field removes dangerous characters)
        $this->assertStringNotContainsString('DROP TABLE', $result);
        $this->assertStringNotContainsString(';', $result);
        $this->assertIsString($result);

        // Test Case 2: SQL injection in email input
        $sql_injection_email = "user@example.com'; DROP TABLE users; --";
        $result = Security_Manager::validate_input($sql_injection_email, 'email');

        // Should return WP_Error for invalid email
        $this->assertInstanceOf(WP_Error::class, $result);
        $this->assertEquals('invalid_email', $result->get_error_code());

        // Test Case 3: SQL injection in integer input
        $sql_injection_int = "1 OR 1=1";
        $result = Security_Manager::validate_input($sql_injection_int, 'int');

        // Should return WP_Error for invalid integer
        $this->assertInstanceOf(WP_Error::class, $result);
        $this->assertEquals('invalid_integer', $result->get_error_code());

        // Test Case 4: SQL injection in URL input
        $sql_injection_url = "http://example.com'; DROP TABLE users; --";
        $result = Security_Manager::validate_input($sql_injection_url, 'url');

        // Should return WP_Error for invalid URL
        $this->assertInstanceOf(WP_Error::class, $result);
        $this->assertEquals('invalid_url', $result->get_error_code());

        // Test Case 5: Valid inputs should pass through
        $valid_inputs = [
            ['john@example.com', 'email'],
            ['123', 'int'],
            ['https://example.com', 'url'],
            ['Clean text input', 'text'],
            ['username123', 'username']
        ];

        foreach ($valid_inputs as [$input, $type]) {
            $result = Security_Manager::validate_input($input, $type);
            $this->assertNotInstanceOf(WP_Error::class, $result);
        }

        // Test Case 6: Complex SQL injection patterns
        $complex_sql_injections = [
            "1' UNION SELECT * FROM wp_users WHERE '1'='1",
            "admin'/**/UNION/**/SELECT/**/password/**/FROM/**/wp_users/**/WHERE/**/user_login='admin'--",
            "1'; INSERT INTO wp_users (user_login, user_pass) VALUES ('hacker', 'password'); --"
        ];

        foreach ($complex_sql_injections as $injection) {
            $result = Security_Manager::validate_input($injection, 'text');
            // Should be sanitized and not contain dangerous SQL keywords
            $this->assertStringNotContainsString('UNION', strtoupper($result));
            $this->assertStringNotContainsString('SELECT', strtoupper($result));
            $this->assertStringNotContainsString('INSERT', strtoupper($result));
        }

        // Test Case 7: Boolean input validation
        $this->assertTrue(Security_Manager::validate_input('true', 'boolean'));
        $this->assertFalse(Security_Manager::validate_input('false', 'boolean'));
        $this->assertInstanceOf(WP_Error::class, Security_Manager::validate_input('not-boolean', 'boolean'));

        // Test Case 8: Float input validation
        $this->assertEquals(123.45, Security_Manager::validate_input('123.45', 'float'));
        $this->assertInstanceOf(WP_Error::class, Security_Manager::validate_input('not-float', 'float'));
    }

    /**
     * Test 5: XSS (Cross-Site Scripting) prevention
     *
     * Tests output sanitization functionality to prevent XSS attacks
     * through the sanitize_output method with various malicious payloads.
     *
     * @covers Security_Manager::sanitize_output
     */
    public function test_xss_prevention(): void {
        // Test Case 1: Basic XSS script tag
        $xss_script = '<script>alert("XSS")</script>';
        $result = Security_Manager::sanitize_output($xss_script, 'text');

        // Should be escaped and not contain script tags
        $this->assertStringNotContainsString('<script>', $result);
        $this->assertStringNotContainsString('alert', $result);
        $this->assertStringContainsString('&lt;script&gt;', $result);

        // Test Case 2: XSS in HTML context
        $xss_html = '<div onclick="alert(\'XSS\')">Click me</div>';
        $result = Security_Manager::sanitize_output($xss_html, 'html');

        // Should remove dangerous attributes but keep safe HTML
        $this->assertStringNotContainsString('onclick', $result);
        $this->assertStringNotContainsString('alert', $result);

        // Test Case 3: XSS in URL context
        $xss_url = 'javascript:alert("XSS")';
        $result = Security_Manager::sanitize_output($xss_url, 'url');

        // Should return empty string for invalid URL
        $this->assertEquals('', $result);

        // Test Case 4: XSS in attribute context
        $xss_attribute = '" onmouseover="alert(\'XSS\')"';
        $result = Security_Manager::sanitize_output($xss_attribute, 'attribute');

        // Should be properly escaped
        $this->assertStringNotContainsString('onmouseover', $result);
        $this->assertStringNotContainsString('alert', $result);

        // Test Case 5: XSS in JavaScript context
        $xss_js = 'alert("XSS"); document.cookie = "stolen";';
        $result = Security_Manager::sanitize_output($xss_js, 'javascript');

        // Should be JSON encoded and safe
        $this->assertStringStartsWith('"', $result);
        $this->assertStringEndsWith('"', $result);
        $this->assertStringContainsString('\\', $result); // Should contain escaped characters

        // Test Case 6: Nested XSS attempts
        $nested_xss = '<img src="x" onerror="<script>alert(\'XSS\')</script>">';
        $result = Security_Manager::sanitize_output($nested_xss, 'html');

        // Should remove all dangerous elements
        $this->assertStringNotContainsString('onerror', $result);
        $this->assertStringNotContainsString('<script>', $result);
        $this->assertStringNotContainsString('alert', $result);

        // Test Case 7: Array/Object sanitization
        $xss_array = [
            'title' => '<script>alert("XSS")</script>',
            'content' => 'Safe content',
            'user' => (object) ['name' => '<img src=x onerror=alert("XSS")>']
        ];

        $result = Security_Manager::sanitize_output($xss_array, 'text');

        // Should recursively sanitize all elements
        $this->assertIsArray($result);
        $this->assertStringNotContainsString('<script>', $result['title']);
        $this->assertEquals('Safe content', $result['content']);
        $this->assertIsObject($result['user']);
        $this->assertStringNotContainsString('<img', $result['user']->name);

        // Test Case 8: Valid content should pass through safely
        $safe_content = [
            'Clean text without any scripts',
            '<p>Safe HTML paragraph</p>',
            'https://example.com',
            'safe-attribute-value'
        ];

        foreach ($safe_content as $content) {
            $result = Security_Manager::sanitize_output($content, 'default');
            $this->assertIsString($result);
            $this->assertNotEmpty($result);
        }

        // Test Case 9: Default context sanitization
        $mixed_content = '<span>Safe</span><script>dangerous()</script>';
        $result = Security_Manager::sanitize_output($mixed_content, 'default');

        $this->assertStringNotContainsString('<script>', $result);
        $this->assertStringNotContainsString('dangerous', $result);
    }

    /**
     * Create mock WP REST Request for testing
     *
     * @param string $method HTTP method
     * @param string $route  Request route
     * @return WP_REST_Request Mock request object
     */
    private function create_mock_request(string $method, string $route): WP_REST_Request {
        $request = new WP_REST_Request($method, $route);
        return $request;
    }

    /**
     * Setup $_SERVER variables for testing
     *
     * @param string $ip         Client IP address
     * @param string $user_agent User agent string
     * @return void
     */
    private function setup_server_vars(string $ip, string $user_agent): void {
        $_SERVER['REMOTE_ADDR'] = $ip;
        $_SERVER['HTTP_USER_AGENT'] = $user_agent;
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_SERVER['HTTP_HOST'] = 'localhost';
    }

    /**
     * Setup transient filters to intercept get_transient/set_transient calls
     *
     * @return void
     */
    private function setup_transient_filters(): void {
        add_filter('pre_transient_care_api_rate_limit_public_127.0.0.1', [$this, 'get_test_transient'], 10, 2);
        add_filter('pre_transient_care_api_rate_limit_public_192.168.1.100', [$this, 'get_test_transient'], 10, 2);
        add_filter('pre_transient_care_api_rate_limit_public_10.0.0.1', [$this, 'get_test_transient'], 10, 2);
        add_filter('pre_transient_care_api_rate_limit_auth_127.0.0.1', [$this, 'get_test_transient'], 10, 2);
        add_filter('pre_transient_care_api_rate_limit_auth_192.168.1.100', [$this, 'get_test_transient'], 10, 2);

        add_filter('pre_set_transient_care_api_rate_limit_public_127.0.0.1', [$this, 'set_test_transient_filter'], 10, 3);
        add_filter('pre_set_transient_care_api_rate_limit_public_192.168.1.100', [$this, 'set_test_transient_filter'], 10, 3);
        add_filter('pre_set_transient_care_api_rate_limit_public_10.0.0.1', [$this, 'set_test_transient_filter'], 10, 3);
        add_filter('pre_set_transient_care_api_rate_limit_auth_127.0.0.1', [$this, 'set_test_transient_filter'], 10, 3);
        add_filter('pre_set_transient_care_api_rate_limit_auth_192.168.1.100', [$this, 'set_test_transient_filter'], 10, 3);
    }

    /**
     * Remove transient filters
     *
     * @return void
     */
    private function remove_transient_filters(): void {
        remove_filter('pre_transient_care_api_rate_limit_public_127.0.0.1', [$this, 'get_test_transient']);
        remove_filter('pre_transient_care_api_rate_limit_public_192.168.1.100', [$this, 'get_test_transient']);
        remove_filter('pre_transient_care_api_rate_limit_public_10.0.0.1', [$this, 'get_test_transient']);
        remove_filter('pre_transient_care_api_rate_limit_auth_127.0.0.1', [$this, 'get_test_transient']);
        remove_filter('pre_transient_care_api_rate_limit_auth_192.168.1.100', [$this, 'get_test_transient']);

        remove_filter('pre_set_transient_care_api_rate_limit_public_127.0.0.1', [$this, 'set_test_transient_filter']);
        remove_filter('pre_set_transient_care_api_rate_limit_public_192.168.1.100', [$this, 'set_test_transient_filter']);
        remove_filter('pre_set_transient_care_api_rate_limit_public_10.0.0.1', [$this, 'set_test_transient_filter']);
        remove_filter('pre_set_transient_care_api_rate_limit_auth_127.0.0.1', [$this, 'set_test_transient_filter']);
        remove_filter('pre_set_transient_care_api_rate_limit_auth_192.168.1.100', [$this, 'set_test_transient_filter']);
    }

    /**
     * Get test transient callback for filters
     *
     * @param mixed  $pre_transient The default value to return if the transient does not exist.
     * @param string $transient     Transient name.
     * @return mixed
     */
    public function get_test_transient($pre_transient, $transient) {
        $full_key = 'pre_transient_' . $transient;
        return $this->test_transients[$full_key] ?? false;
    }

    /**
     * Set test transient callback for filters
     *
     * @param mixed  $value      New value of transient.
     * @param int    $expiration Time until expiration in seconds.
     * @param string $transient  Transient name.
     * @return mixed
     */
    public function set_test_transient_filter($value, $expiration, $transient) {
        $full_key = 'pre_transient_' . $transient;
        $this->test_transients[$full_key] = $value;
        return true;
    }

    /**
     * Set test transient manually
     *
     * @param string $key   Transient key
     * @param mixed  $value Transient value
     * @return void
     */
    private function set_test_transient(string $key, $value): void {
        $full_key = 'pre_transient_' . $key;
        $this->test_transients[$full_key] = $value;
    }
}