<?php

declare(strict_types=1);

use App\Framework\Http\Session\SessionInterface;
use App\Framework\LiveComponents\ComponentEventDispatcher;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\LiveComponentHandler;
use App\Framework\LiveComponents\ValueObjects\ActionParameters;
use App\Framework\LiveComponents\ValueObjects\ComponentAction;
use App\Framework\LiveComponents\ValueObjects\ComponentData;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\Security\CsrfToken;
use App\Framework\View\LiveComponentRenderer;
use App\Framework\View\TemplateRenderer;

beforeEach(function () {
    // Create mock SessionInterface for CSRF testing
    $this->session = new class () implements SessionInterface {
        private array $tokens = [];

        public function __get(string $name): mixed
        {
            if ($name === 'csrf') {
                return new class ($this) {
                    public function __construct(private $session)
                    {
                    }

                    public function generateToken(string $formId): CsrfToken
                    {
                        $token = CsrfToken::generate();
                        $this->session->tokens[$formId] = $token->toString();

                        return $token;
                    }

                    public function validateToken(string $formId, CsrfToken $token): bool
                    {
                        return isset($this->session->tokens[$formId])
                            && hash_equals($this->session->tokens[$formId], $token->toString());
                    }
                };
            }

            return null;
        }

        public function get(string $key, mixed $default = null): mixed
        {
            return $default;
        }

        public function set(string $key, mixed $value): void
        {
        }

        public function has(string $key): bool
        {
            return false;
        }

        public function remove(string $key): void
        {
        }

        public function regenerate(): bool
        {
            return true;
        }

        public function destroy(): void
        {
        }

        public function getId(): string
        {
            return 'test-session-id';
        }
    };

    $this->eventDispatcher = new ComponentEventDispatcher();
    $this->handler = new LiveComponentHandler($this->eventDispatcher, $this->session);
});

describe('CSRF Token Generation', function () {
    it('generates unique CSRF token for each component instance', function () {
        $renderer = new LiveComponentRenderer(
            $this->createMock(TemplateRenderer::class),
            $this->session
        );

        $html1 = $renderer->renderWithWrapper(
            'counter:instance1',
            '<div>Component 1</div>',
            ['count' => 0]
        );

        $html2 = $renderer->renderWithWrapper(
            'counter:instance2',
            '<div>Component 2</div>',
            ['count' => 0]
        );

        // Both should have CSRF tokens
        expect($html1)->toContain('data-csrf-token');
        expect($html2)->toContain('data-csrf-token');

        // Extract tokens (they should be different)
        preg_match('/data-csrf-token="([^"]+)"/', $html1, $matches1);
        preg_match('/data-csrf-token="([^"]+)"/', $html2, $matches2);

        expect($matches1[1])->not->toBe($matches2[1]);
    });

    it('generates CSRF token with correct formId pattern', function () {
        $componentId = 'counter:test123';
        $expectedFormId = 'livecomponent:counter:test123';

        // Generate token using the session
        $token = $this->session->csrf->generateToken($expectedFormId);

        expect($token)->toBeInstanceOf(CsrfToken::class);
        expect($token->toString())->toHaveLength(32);
    });

    it('includes CSRF token in rendered wrapper HTML', function () {
        $renderer = new LiveComponentRenderer(
            $this->createMock(TemplateRenderer::class),
            $this->session
        );

        $html = $renderer->renderWithWrapper(
            'counter:csrf-test',
            '<div>Test Component</div>',
            ['count' => 42]
        );

        // Should contain data-csrf-token attribute
        expect($html)->toMatch('/data-csrf-token="[a-f0-9]{32}"/');

        // Should contain component ID and state
        expect($html)->toContain('data-live-component="counter:csrf-test"');
        expect($html)->toContain('data-state');
    });
});

describe('CSRF Token Validation', function () {
    it('validates correct CSRF token', function () {
        $componentId = ComponentId::fromString('counter:validation-test');
        $formId = 'livecomponent:counter:validation-test';

        // Generate valid token
        $csrfToken = $this->session->csrf->generateToken($formId);

        // Create ActionParameters with valid token
        $params = ActionParameters::fromArray([], $csrfToken);

        // Create test component
        $component = new class ($componentId) implements LiveComponentContract {
            public function __construct(private ComponentId $id)
            {
            }

            public function getId(): ComponentId
            {
                return $this->id;
            }

            public function getData(): ComponentData
            {
                return ComponentData::fromArray([]);
            }

            public function increment(): ComponentData
            {
                return ComponentData::fromArray(['count' => 1]);
            }
        };

        // Should not throw exception
        $result = $this->handler->handle($component, 'increment', $params);
        expect($result)->not->toBeNull();
    });

    it('rejects request without CSRF token', function () {
        $componentId = ComponentId::fromString('counter:no-token-test');

        // Create ActionParameters WITHOUT token
        $params = ActionParameters::fromArray([]);

        $component = new class ($componentId) implements LiveComponentContract {
            public function __construct(private ComponentId $id)
            {
            }

            public function getId(): ComponentId
            {
                return $this->id;
            }

            public function getData(): ComponentData
            {
                return ComponentData::fromArray([]);
            }

            public function action(): ComponentData
            {
                return ComponentData::fromArray([]);
            }
        };

        expect(fn () => $this->handler->handle($component, 'action', $params))
            ->toThrow(\InvalidArgumentException::class, 'CSRF token is required');
    });

    it('rejects request with invalid CSRF token', function () {
        $componentId = ComponentId::fromString('counter:invalid-token-test');

        // Create INVALID token (not generated by session)
        $invalidToken = CsrfToken::fromString(bin2hex(random_bytes(16)));
        $params = ActionParameters::fromArray([], $invalidToken);

        $component = new class ($componentId) implements LiveComponentContract {
            public function __construct(private ComponentId $id)
            {
            }

            public function getId(): ComponentId
            {
                return $this->id;
            }

            public function getData(): ComponentData
            {
                return ComponentData::fromArray([]);
            }

            public function action(): ComponentData
            {
                return ComponentData::fromArray([]);
            }
        };

        expect(fn () => $this->handler->handle($component, 'action', $params))
            ->toThrow(\RuntimeException::class, 'CSRF token validation failed');
    });

    it('validates CSRF token for different component instances separately', function () {
        // Generate token for component A
        $componentIdA = ComponentId::fromString('counter:instanceA');
        $formIdA = 'livecomponent:counter:instanceA';
        $tokenA = $this->session->csrf->generateToken($formIdA);

        // Try to use token A with component B (should fail)
        $componentIdB = ComponentId::fromString('counter:instanceB');
        $params = ActionParameters::fromArray([], $tokenA);

        $componentB = new class ($componentIdB) implements LiveComponentContract {
            public function __construct(private ComponentId $id)
            {
            }

            public function getId(): ComponentId
            {
                return $this->id;
            }

            public function getData(): ComponentData
            {
                return ComponentData::fromArray([]);
            }

            public function action(): ComponentData
            {
                return ComponentData::fromArray([]);
            }
        };

        expect(fn () => $this->handler->handle($componentB, 'action', $params))
            ->toThrow(\RuntimeException::class, 'CSRF token validation failed');
    });
});

describe('ComponentAction CSRF Extraction', function () {
    it('extracts CSRF token from request body', function () {
        $mockRequest = new class () {
            public ?object $parsedBody = null;

            public object $headers;

            public function __construct()
            {
                $this->parsedBody = (object)[
                    'toArray' => fn () => [
                        'component_id' => 'counter:test',
                        'method' => 'increment',
                        '_csrf_token' => '0123456789abcdef0123456789abcdef',
                        'params' => [],
                    ],
                ];

                $this->headers = new class () {
                    public function getFirst(string $key): ?string
                    {
                        return null;
                    }
                };
            }
        };

        $action = ComponentAction::fromRequest($mockRequest);

        expect($action->params->hasCsrfToken())->toBeTrue();
        expect($action->params->getCsrfToken()->toString())->toBe('0123456789abcdef0123456789abcdef');
    });

    it('extracts CSRF token from X-CSRF-Token header', function () {
        $mockRequest = new class () {
            public ?object $parsedBody = null;

            public object $headers;

            public function __construct()
            {
                $this->parsedBody = (object)[
                    'toArray' => fn () => [
                        'component_id' => 'counter:test',
                        'method' => 'increment',
                        'params' => [],
                    ],
                ];

                $this->headers = new class () {
                    public function getFirst(string $key): ?string
                    {
                        return $key === 'X-CSRF-Token'
                            ? 'fedcba9876543210fedcba9876543210'
                            : null;
                    }
                };
            }
        };

        $action = ComponentAction::fromRequest($mockRequest);

        expect($action->params->hasCsrfToken())->toBeTrue();
        expect($action->params->getCsrfToken()->toString())->toBe('fedcba9876543210fedcba9876543210');
    });

    it('handles missing CSRF token gracefully', function () {
        $mockRequest = new class () {
            public ?object $parsedBody = null;

            public object $headers;

            public function __construct()
            {
                $this->parsedBody = (object)[
                    'toArray' => fn () => [
                        'component_id' => 'counter:test',
                        'method' => 'increment',
                        'params' => [],
                    ],
                ];

                $this->headers = new class () {
                    public function getFirst(string $key): ?string
                    {
                        return null;
                    }
                };
            }
        };

        $action = ComponentAction::fromRequest($mockRequest);

        expect($action->params->hasCsrfToken())->toBeFalse();
        expect($action->params->getCsrfToken())->toBeNull();
    });
});

describe('File Upload CSRF Protection', function () {
    it('validates CSRF token for file uploads', function () {
        $componentId = ComponentId::fromString('uploader:test');
        $formId = 'livecomponent:uploader:test';

        // Generate valid token
        $csrfToken = $this->session->csrf->generateToken($formId);
        $params = ActionParameters::fromArray([], $csrfToken);

        // Mock UploadedFile
        $file = new class () {
            public string $name = 'test.txt';

            public int $size = 1024;

            public string $tmpName = '/tmp/test';

            public int $error = 0;
        };

        // Create component that supports file upload
        $component = new class ($componentId) implements LiveComponentContract, \App\Framework\LiveComponents\Contracts\SupportsFileUpload {
            public function __construct(private ComponentId $id)
            {
            }

            public function getId(): ComponentId
            {
                return $this->id;
            }

            public function getData(): ComponentData
            {
                return ComponentData::fromArray([]);
            }

            public function handleUpload($file, ActionParameters $params, ComponentEventDispatcher $dispatcher): ComponentData
            {
                return ComponentData::fromArray(['uploaded' => true]);
            }
        };

        // Should not throw exception
        $result = $this->handler->handleUpload($component, $file, $params);
        expect($result)->not->toBeNull();
    });

    it('rejects file upload without CSRF token', function () {
        $componentId = ComponentId::fromString('uploader:no-token');
        $params = ActionParameters::fromArray([]);

        $file = new class () {
            public string $name = 'test.txt';
        };

        $component = new class ($componentId) implements LiveComponentContract, \App\Framework\LiveComponents\Contracts\SupportsFileUpload {
            public function __construct(private ComponentId $id)
            {
            }

            public function getId(): ComponentId
            {
                return $this->id;
            }

            public function getData(): ComponentData
            {
                return ComponentData::fromArray([]);
            }

            public function handleUpload($file, ActionParameters $params, ComponentEventDispatcher $dispatcher): ComponentData
            {
                return ComponentData::fromArray([]);
            }
        };

        expect(fn () => $this->handler->handleUpload($component, $file, $params))
            ->toThrow(\InvalidArgumentException::class, 'CSRF token is required');
    });
});

describe('End-to-End CSRF Flow', function () {
    it('completes full CSRF flow from render to action execution', function () {
        // Step 1: Render component with CSRF token
        $renderer = new LiveComponentRenderer(
            $this->createMock(TemplateRenderer::class),
            $this->session
        );

        $componentId = 'counter:e2e-test';
        $html = $renderer->renderWithWrapper(
            $componentId,
            '<div>Counter: 0</div>',
            ['count' => 0]
        );

        // Step 2: Extract CSRF token from rendered HTML
        preg_match('/data-csrf-token="([^"]+)"/', $html, $matches);
        expect($matches)->toHaveCount(2);
        $csrfTokenString = $matches[1];

        // Step 3: Simulate client sending action with CSRF token
        $csrfToken = CsrfToken::fromString($csrfTokenString);
        $params = ActionParameters::fromArray(['amount' => 1], $csrfToken);

        // Step 4: Execute action with CSRF validation
        $component = new class (ComponentId::fromString($componentId)) implements LiveComponentContract {
            public function __construct(private ComponentId $id, private int $count = 0)
            {
            }

            public function getId(): ComponentId
            {
                return $this->id;
            }

            public function getData(): ComponentData
            {
                return ComponentData::fromArray(['count' => $this->count]);
            }

            public function increment(int $amount): ComponentData
            {
                $this->count += $amount;

                return $this->getData();
            }
        };

        $result = $this->handler->handle($component, 'increment', $params);

        // Step 5: Verify action executed successfully
        expect($result)->not->toBeNull();
        expect($result->state->data['count'])->toBe(1);
    });
});
