<?php
/**
 * HMAC Authentication Test Cases for CuliUptime PHP Agent
 *
 * Based on US004-agent-management.md Scenario 3 & AC004-agent-management.feature
 * Tests HMAC signature authentication requirements
 */

class AgentHMACTest
{
    private $testResults = [];
    private $testAgent;

    public function runAllTests()
    {
        echo "=== CuliUptime Agent HMAC Authentication Tests ===\n";

        // Initialize test environment
        $this->setupTestEnvironment();

        // Security tests FIRST (TDD requirement)
        $this->testHMACSignatureValidation();
        $this->testTimestampValidation();
        $this->testReplayAttackPrevention();
        $this->testInvalidSignatureRejection();
        $this->testMissingHeadersRejection();

        // Validation tests SECOND
        $this->testCanonicalRequestGeneration();
        $this->testSignatureFormat();

        // Functional tests LAST
        $this->testValidHMACRequest();
        $this->testPlatformIdentityVerification();

        $this->printResults();
    }

    private function setupTestEnvironment() {
        // Mock the agent configuration for testing
        global $AGENT_CONFIG;
        $AGENT_CONFIG = [
            'version' => '0.1.1',
            'agent_id' => 'test-agent-uuid-12345',
            'shared_secret' => 'test-shared-secret-64-chars-long-for-hmac-authentication-tests',
            'platform_api_key' => 'platform-api-key-for-testing',
            'security' => [
                'hmac_enabled' => true,
                'timestamp_window' => 300, // 5 minutes
            ]
        ];
    }

    /**
     * Test Case: HMAC Signature Validation
     * US004 Requirement: Agents verify platform identity using shared secret
     * AC004: Agent should validate platform identity using shared secret
     */
    public function testHMACSignatureValidation()
    {
        $testName = "HMAC Signature Validation";

        $timestamp = time();
        $requestBody = '{"task_id":"test-123","type":"health_check","timeout":30}';
        $canonicalRequest = "POST\nhttp://agent.local/culiuptime-agent.php\n{$timestamp}\n{$requestBody}\nplatform-api-key-for-testing";

        // Generate valid HMAC signature
        $validSignature = hash_hmac('sha256', $canonicalRequest, 'test-shared-secret-64-chars-long-for-hmac-authentication-tests');

        $headers = [
            // 'X-CuliOps-API-Key' => 'platform-api-key-for-testing',  // REMOVED - no longer transmitted
            'X-CuliOps-Agent-ID' => 'test-agent-uuid-12345',
            'X-CuliOps-Signature' => $validSignature,
            'X-CuliOps-Timestamp' => $timestamp,
            'Content-Type' => 'application/json'
        ];

        // This will fail initially - need to implement validateHMACAuthentication function
        $result = $this->simulateHMACRequest($requestBody, $headers);

        $passed = $result['authenticated'] === true;
        $this->recordResult($testName, $passed,
            "Expected: authentication success. Actual: " .
            ($result['authenticated'] ? 'success' : 'failed - ' . ($result['error'] ?? 'unknown'))
        );
    }

    /**
     * Test Case: Timestamp Validation
     * US004 Requirement: Timestamp-based request validation (5-minute window)
     * AC004: Timestamp validation should prevent replay attacks (5-minute window)
     */
    public function testTimestampValidation()
    {
        $testName = "Timestamp Validation";

        $validTimestamp = time(); // Current time
        $expiredTimestamp = time() - 400; // 6 minutes ago (should fail)
        $futureTimestamp = time() + 400; // 6 minutes in future (should fail)

        $requestBody = '{"task_id":"test-timestamp","type":"health_check"}';

        // Test with valid timestamp
        $result1 = $this->generateHMACRequest($requestBody, $validTimestamp);
        $validResult = $this->simulateHMACRequest($requestBody, $result1['headers']);

        // Test with expired timestamp
        $result2 = $this->generateHMACRequest($requestBody, $expiredTimestamp);
        $expiredResult = $this->simulateHMACRequest($requestBody, $result2['headers']);

        // Test with future timestamp
        $result3 = $this->generateHMACRequest($requestBody, $futureTimestamp);
        $futureResult = $this->simulateHMACRequest($requestBody, $result3['headers']);

        $passed = $validResult['authenticated'] &&
                 !$expiredResult['authenticated'] &&
                 !$futureResult['authenticated'];

        $this->recordResult($testName, $passed,
            "Expected: valid timestamp accepted, expired/future rejected. " .
            "Results: valid=" . ($validResult['authenticated'] ? 'accepted' : 'rejected') .
            ", expired=" . ($expiredResult['authenticated'] ? 'accepted' : 'rejected') .
            ", future=" . ($futureResult['authenticated'] ? 'accepted' : 'rejected')
        );
    }

    /**
     * Test Case: Replay Attack Prevention
     * US004 Requirement: Replay attack prevention via timestamp validation
     */
    public function testReplayAttackPrevention()
    {
        $testName = "Replay Attack Prevention";

        $timestamp = time();
        $requestBody = '{"task_id":"replay-test","type":"health_check"}';

        $result = $this->generateHMACRequest($requestBody, $timestamp);

        // First request should succeed
        $firstResult = $this->simulateHMACRequest($requestBody, $result['headers']);

        // Same request with same timestamp should succeed (stateless for MVP)
        // But in practice, expired timestamps will be rejected
        $replayResult = $this->simulateHMACRequest($requestBody, $result['headers']);

        // Test with timestamp that's too old (replay prevention)
        $oldTimestamp = $timestamp - 350; // ~6 minutes old
        $oldResult = $this->generateHMACRequest($requestBody, $oldTimestamp);
        $oldReplayResult = $this->simulateHMACRequest($requestBody, $oldResult['headers']);

        $passed = $firstResult['authenticated'] &&
                 $replayResult['authenticated'] && // Same timestamp is OK for stateless agent
                 !$oldReplayResult['authenticated']; // Old timestamp should fail

        $this->recordResult($testName, $passed,
            "Expected: current timestamp OK, old timestamp rejected. " .
            "Results: first=" . ($firstResult['authenticated'] ? 'OK' : 'rejected') .
            ", replay=" . ($replayResult['authenticated'] ? 'OK' : 'rejected') .
            ", old=" . ($oldReplayResult['authenticated'] ? 'OK' : 'rejected')
        );
    }

    /**
     * Test Case: Invalid Signature Rejection
     * US004 Requirement: Agents authenticate platform requests using HMAC-SHA256
     */
    public function testInvalidSignatureRejection()
    {
        $testName = "Invalid Signature Rejection";

        $timestamp = time();
        $requestBody = '{"task_id":"invalid-sig","type":"health_check"}';

        $validResult = $this->generateHMACRequest($requestBody, $timestamp);

        // Test with wrong signature
        $invalidHeaders = $validResult['headers'];
        $invalidHeaders['X-CuliOps-Signature'] = 'invalid-signature-that-should-fail';

        // Test with wrong shared secret
        $wrongSecretSignature = hash_hmac('sha256',
            "POST\n/culiuptime-agent.php\n{$timestamp}\n{$requestBody}",
            'wrong-shared-secret'
        );
        $wrongSecretHeaders = $validResult['headers'];
        $wrongSecretHeaders['X-CuliOps-Signature'] = $wrongSecretSignature;

        $validResult = $this->simulateHMACRequest($requestBody, $validResult['headers']);
        $invalidResult = $this->simulateHMACRequest($requestBody, $invalidHeaders);
        $wrongSecretResult = $this->simulateHMACRequest($requestBody, $wrongSecretHeaders);

        $passed = $validResult['authenticated'] &&
                 !$invalidResult['authenticated'] &&
                 !$wrongSecretResult['authenticated'];

        $this->recordResult($testName, $passed,
            "Expected: valid signature accepted, invalid signatures rejected. " .
            "Results: valid=" . ($validResult['authenticated'] ? 'accepted' : 'rejected') .
            ", invalid=" . ($invalidResult['authenticated'] ? 'accepted' : 'rejected') .
            ", wrong_secret=" . ($wrongSecretResult['authenticated'] ? 'accepted' : 'rejected')
        );
    }

    /**
     * Test Case: Missing Headers Rejection
     * US004 Requirement: Include platform API key, agent ID, signature, and timestamp headers
     */
    public function testMissingHeadersRejection()
    {
        $testName = "Missing Headers Rejection";

        $requestBody = '{"task_id":"missing-headers","type":"health_check"}';
        $result = $this->generateHMACRequest($requestBody, time());
        $completeHeaders = $result['headers'];

        // API key test removed - no longer required header

        // Test missing Agent ID
        $missingAgentId = $completeHeaders;
        unset($missingAgentId['X-CuliOps-Agent-ID']);

        // Test missing Signature
        $missingSignature = $completeHeaders;
        unset($missingSignature['X-CuliOps-Signature']);

        // Test missing Timestamp
        $missingTimestamp = $completeHeaders;
        unset($missingTimestamp['X-CuliOps-Timestamp']);

        $completeResult = $this->simulateHMACRequest($requestBody, $completeHeaders);
        // API key test removed
        $missingAgentResult = $this->simulateHMACRequest($requestBody, $missingAgentId);
        $missingSigResult = $this->simulateHMACRequest($requestBody, $missingSignature);
        $missingTsResult = $this->simulateHMACRequest($requestBody, $missingTimestamp);

        $passed = $completeResult['authenticated'] &&
                 !$missingAgentResult['authenticated'] &&
                 !$missingSigResult['authenticated'] &&
                 !$missingTsResult['authenticated'];

        $this->recordResult($testName, $passed,
            "Expected: complete headers accepted, missing headers rejected. " .
            "Results: complete=" . ($completeResult['authenticated'] ? 'OK' : 'rejected') .
            ", missing_agent=" . ($missingAgentResult['authenticated'] ? 'OK' : 'rejected') .
            ", missing_sig=" . ($missingSigResult['authenticated'] ? 'OK' : 'rejected') .
            ", missing_ts=" . ($missingTsResult['authenticated'] ? 'OK' : 'rejected')
        );
    }

    /**
     * Test Case: Canonical Request Generation
     * US004 Requirement: HMAC signature generation format
     */
    public function testCanonicalRequestGeneration()
    {
        $testName = "Canonical Request Generation";

        $method = 'POST';
        $url = 'http://agent.local/culiuptime-agent.php';
        $timestamp = 1691648400;
        $body = '{"task_id":"canonical-test","type":"health_check"}';

        $expectedCanonical = "POST\nhttp://agent.local/culiuptime-agent.php\n1691648400\n{\"task_id\":\"canonical-test\",\"type\":\"health_check\"}\nplatform-api-key-for-testing";

        $actualCanonical = $this->generateCanonicalRequest($method, $url, $timestamp, $body);

        $passed = $actualCanonical === $expectedCanonical;
        $this->recordResult($testName, $passed,
            "Expected: '$expectedCanonical'. " .
            "Actual: '$actualCanonical'"
        );
    }

    /**
     * Test Case: Signature Format
     * US004 Requirement: HMAC-SHA256 signature format validation
     */
    public function testSignatureFormat()
    {
        $testName = "Signature Format";

        $canonicalRequest = "POST\n/culiuptime-agent.php\n1691648400\ntest-body\nplatform-api-key-for-testing";
        $sharedSecret = 'test-secret';

        $signature = hash_hmac('sha256', $canonicalRequest, $sharedSecret);

        // HMAC-SHA256 should produce 64-character hex string
        $passed = is_string($signature) &&
                 strlen($signature) === 64 &&
                 ctype_xdigit($signature);

        $this->recordResult($testName, $passed,
            "Expected: 64-character hex string. " .
            "Actual: '" . $signature . "' (length: " . strlen($signature) . ", hex: " . (ctype_xdigit($signature) ? 'yes' : 'no') . ")"
        );
    }

    /**
     * Test Case: Valid HMAC Request
     * US004 Requirement: Complete HMAC authentication flow
     */
    public function testValidHMACRequest()
    {
        $testName = "Valid HMAC Request";

        $requestBody = '{"task_id":"valid-test","type":"health_check","timeout":30}';
        $result = $this->generateHMACRequest($requestBody, time());

        $response = $this->simulateHMACRequest($requestBody, $result['headers']);

        $passed = $response['authenticated'] === true;
        $this->recordResult($testName, $passed,
            "Expected: successful authentication. " .
            "Actual: " . ($response['authenticated'] ? 'success' : 'failed - ' . ($response['error'] ?? 'unknown'))
        );
    }

    /**
     * Test Case: Platform Identity Verification
     * US004 Requirement: Verify platform API key and agent ID matching
     */
    public function testPlatformIdentityVerification()
    {
        $testName = "Platform Identity Verification";

        $requestBody = '{"task_id":"platform-test","type":"health_check"}';
        $result = $this->generateHMACRequest($requestBody, time());

        // Test with correct platform API key
        $validResult = $this->simulateHMACRequest($requestBody, $result['headers']);

        // Platform API key test removed - no longer validated

        // Test with wrong agent ID
        $wrongAgentHeaders = $result['headers'];
        $wrongAgentHeaders['X-CuliOps-Agent-ID'] = 'wrong-agent-uuid';
        $wrongAgentResult = $this->simulateHMACRequest($requestBody, $wrongAgentHeaders);

        $passed = $validResult['authenticated'] &&
                 !$wrongAgentResult['authenticated'];

        $this->recordResult($testName, $passed,
            "Expected: correct credentials accepted, wrong agent ID rejected. " .
            "Results: valid=" . ($validResult['authenticated'] ? 'accepted' : 'rejected') .
            ", wrong_agent=" . ($wrongAgentResult['authenticated'] ? 'accepted' : 'rejected')
        );
    }

    // Helper functions for testing

    private function generateHMACRequest($requestBody, $timestamp)
    {
        global $AGENT_CONFIG;

        $canonicalRequest = $this->generateCanonicalRequest('POST', 'http://agent.local/culiuptime-agent.php', $timestamp, $requestBody);
        $signature = hash_hmac('sha256', $canonicalRequest, $AGENT_CONFIG['shared_secret']);

        return [
            'canonical_request' => $canonicalRequest,
            'headers' => [
                // 'X-CuliOps-API-Key' => $AGENT_CONFIG['platform_api_key'],  // REMOVED - no longer transmitted
                'X-CuliOps-Agent-ID' => $AGENT_CONFIG['agent_id'],
                'X-CuliOps-Signature' => $signature,
                'X-CuliOps-Timestamp' => $timestamp,
                'Content-Type' => 'application/json'
            ]
        ];
    }

    private function generateCanonicalRequest($method, $url, $timestamp, $body)
    {
        global $AGENT_CONFIG;
        // Platform API key included in HMAC but not transmitted for security
        return "{$method}\n{$url}\n{$timestamp}\n{$body}\n{$AGENT_CONFIG['platform_api_key']}";
    }

    private function simulateHMACRequest($requestBody, $headers)
    {
        // Directly implement the HMAC validation logic for testing
        global $AGENT_CONFIG;

        try {
            // Check if HMAC is enabled
            if (!$AGENT_CONFIG['security']['hmac_enabled']) {
                return [
                    'authenticated' => false,
                    'error' => 'HMAC authentication is not enabled'
                ];
            }

            // Check required HMAC headers
            $requiredHeaders = [
                // 'X-CuliOps-API-Key',  // REMOVED - no longer required
                'X-CuliOps-Agent-ID',
                'X-CuliOps-Signature',
                'X-CuliOps-Timestamp'
            ];

            foreach ($requiredHeaders as $header) {
                if (!isset($headers[$header]) || empty($headers[$header])) {
                    return [
                        'authenticated' => false,
                        'error' => "Missing required header: $header"
                    ];
                }
            }

            // Platform API key validation removed - key included in HMAC signature only

            // Validate agent ID
            if ($headers['X-CuliOps-Agent-ID'] !== $AGENT_CONFIG['agent_id']) {
                return [
                    'authenticated' => false,
                    'error' => 'Invalid agent ID'
                ];
            }

            // Validate timestamp (replay attack prevention)
            $timestamp = intval($headers['X-CuliOps-Timestamp']);
            $currentTime = time();
            $timestampWindow = $AGENT_CONFIG['security']['timestamp_window'];

            if (abs($currentTime - $timestamp) > $timestampWindow) {
                return [
                    'authenticated' => false,
                    'error' => 'Request timestamp outside allowed window'
                ];
            }

            // Generate canonical request for signature verification
            $method = 'POST';
            $url = 'http://agent.local/culiuptime-agent.php';
            $canonicalRequest = $this->generateCanonicalRequest($method, $url, $timestamp, $requestBody);

            // Calculate expected signature
            $expectedSignature = hash_hmac('sha256', $canonicalRequest, $AGENT_CONFIG['shared_secret']);
            $providedSignature = $headers['X-CuliOps-Signature'];

            // Verify signature using timing-safe comparison
            if (!hash_equals($expectedSignature, $providedSignature)) {
                return [
                    'authenticated' => false,
                    'error' => 'Invalid HMAC signature'
                ];
            }

            return [
                'authenticated' => true,
                'agent_id' => $headers['X-CuliOps-Agent-ID'],
                'timestamp' => $timestamp
            ];

        } catch (Exception $e) {
            return [
                'authenticated' => false,
                'error' => 'HMAC validation error: ' . $e->getMessage()
            ];
        }
    }

    private function recordResult($testName, $passed, $details = '')
    {
        $this->testResults[] = [
            'name' => $testName,
            'passed' => $passed,
            'details' => $details
        ];

        echo ($passed ? "✅" : "❌") . " $testName\n";
        if (!$passed && $details) {
            echo "   Details: $details\n";
        }
    }

    private function printResults()
    {
        $totalTests = count($this->testResults);
        $passedTests = count(array_filter($this->testResults, fn($r) => $r['passed']));
        $failedTests = $totalTests - $passedTests;

        echo "\n=== HMAC Authentication Test Results ===\n";
        echo "Total Tests: $totalTests\n";
        echo "Passed: $passedTests\n";
        echo "Failed: $failedTests\n";

        if ($failedTests > 0) {
            echo "\n❌ Failed Tests:\n";
            foreach ($this->testResults as $result) {
                if (!$result['passed']) {
                    echo "- {$result['name']}: {$result['details']}\n";
                }
            }
        }

        echo ($failedTests === 0 ? "\n✅ All HMAC tests passed!\n" : "\n❌ Some HMAC tests failed - implementation needed\n");
    }
}

// Run tests if called directly
if (basename(__FILE__) === basename($_SERVER['SCRIPT_NAME'])) {
    $test = new AgentHMACTest();
    $test->runAllTests();
}

?>
