<?php

declare(strict_types=1);

namespace Tests\Framework\Http\Parser;

use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Http\Parser\ParserConfig;
use App\Framework\Http\Parser\StreamingParser;
use PHPUnit\Framework\TestCase;

/**
 * Tests for the streaming multipart parser with generators
 */
final class StreamingParserTest extends TestCase
{
    private StreamingParser $parser;

    protected function setUp(): void
    {
        $this->parser = new StreamingParser();
    }

    protected function tearDown(): void
    {
        // Clean up any temp files
        $tempFiles = glob(sys_get_temp_dir() . '/upload_*');
        foreach ($tempFiles as $file) {
            if (is_file($file)) {
                unlink($file);
            }
        }
    }

    public function testStreamSimpleFormField(): void
    {
        $boundary = 'boundary123';
        $data = "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"field1\"\r\n";
        $data .= "\r\n";
        $data .= "value1\r\n";
        $data .= "--boundary123--\r\n";

        $stream = $this->createStream($data);
        $parts = iterator_to_array($this->parser->streamMultipart($stream, $boundary));
        fclose($stream);

        $this->assertCount(1, $parts);
        $this->assertEquals('field', $parts[0]['type']);
        $this->assertEquals('field1', $parts[0]['name']);
        $this->assertEquals('value1', $parts[0]['data']);
    }

    public function testStreamMultipleFields(): void
    {
        $boundary = 'boundary123';
        $data = "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"field1\"\r\n";
        $data .= "\r\n";
        $data .= "value1\r\n";
        $data .= "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"field2\"\r\n";
        $data .= "\r\n";
        $data .= "value2\r\n";
        $data .= "--boundary123--\r\n";

        $stream = $this->createStream($data);
        $parts = iterator_to_array($this->parser->streamMultipart($stream, $boundary));
        fclose($stream);

        $this->assertCount(2, $parts);
        $this->assertEquals('field1', $parts[0]['name']);
        $this->assertEquals('value1', $parts[0]['data']);
        $this->assertEquals('field2', $parts[1]['name']);
        $this->assertEquals('value2', $parts[1]['data']);
    }

    public function testStreamFileUpload(): void
    {
        $boundary = 'boundary123';
        $fileContent = "This is the file content\nWith multiple lines";

        $data = "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"upload\"; filename=\"test.txt\"\r\n";
        $data .= "Content-Type: text/plain\r\n";
        $data .= "\r\n";
        $data .= $fileContent . "\r\n";
        $data .= "--boundary123--\r\n";

        $stream = $this->createStream($data);
        $parts = iterator_to_array($this->parser->streamMultipart($stream, $boundary));
        fclose($stream);

        $this->assertCount(1, $parts);
        $this->assertEquals('file', $parts[0]['type']);
        $this->assertEquals('upload', $parts[0]['name']);
        $this->assertEquals('test.txt', $parts[0]['filename']);
        $this->assertIsResource($parts[0]['stream']);

        // Read content from temp file stream
        $content = stream_get_contents($parts[0]['stream']);
        fclose($parts[0]['stream']);

        $this->assertEquals($fileContent . "\r\n", $content);
    }

    public function testStreamLargeFile(): void
    {
        $boundary = 'boundary123';
        // Generate 1MB of data
        $fileContent = str_repeat('A', 1024 * 1024);

        $data = "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"bigfile\"; filename=\"large.bin\"\r\n";
        $data .= "Content-Type: application/octet-stream\r\n";
        $data .= "\r\n";
        $data .= $fileContent . "\r\n";
        $data .= "--boundary123--\r\n";

        $stream = $this->createStream($data);

        // Should stream without loading entire file into memory
        $memoryBefore = memory_get_usage();
        $parts = [];

        foreach ($this->parser->streamMultipart($stream, $boundary) as $part) {
            $parts[] = $part;

            // Memory usage should not increase significantly
            $memoryDuring = memory_get_usage();
            $memoryIncrease = $memoryDuring - $memoryBefore;

            // Should use less than 100KB extra memory for streaming
            $this->assertLessThan(
                100 * 1024,
                $memoryIncrease,
                'Streaming should not load entire file into memory'
            );
        }

        fclose($stream);

        $this->assertCount(1, $parts);
        $this->assertEquals('file', $parts[0]['type']);
        $this->assertEquals('bigfile', $parts[0]['name']);

        // Verify file size
        $stats = fstat($parts[0]['stream']);
        $this->assertEquals(strlen($fileContent) + 2, $stats['size']); // +2 for CRLF

        fclose($parts[0]['stream']);
    }

    public function testStreamMixedContent(): void
    {
        $boundary = 'boundary123';

        $data = "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"text\"\r\n";
        $data .= "\r\n";
        $data .= "Some text value\r\n";
        $data .= "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"doc.pdf\"\r\n";
        $data .= "Content-Type: application/pdf\r\n";
        $data .= "\r\n";
        $data .= "%PDF-1.4 fake pdf content\r\n";
        $data .= "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"another\"\r\n";
        $data .= "\r\n";
        $data .= "Another field\r\n";
        $data .= "--boundary123--\r\n";

        $stream = $this->createStream($data);
        $parts = iterator_to_array($this->parser->streamMultipart($stream, $boundary));
        fclose($stream);

        $this->assertCount(3, $parts);

        // First part - text field
        $this->assertEquals('field', $parts[0]['type']);
        $this->assertEquals('text', $parts[0]['name']);
        $this->assertEquals('Some text value', $parts[0]['data']);

        // Second part - file
        $this->assertEquals('file', $parts[1]['type']);
        $this->assertEquals('file', $parts[1]['name']);
        $this->assertEquals('doc.pdf', $parts[1]['filename']);

        // Third part - another field
        $this->assertEquals('field', $parts[2]['type']);
        $this->assertEquals('another', $parts[2]['name']);
        $this->assertEquals('Another field', $parts[2]['data']);

        // Cleanup
        if (isset($parts[1]['stream'])) {
            fclose($parts[1]['stream']);
        }
    }

    public function testParseFilesFromStream(): void
    {
        $boundary = 'boundary123';

        $data = "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"files[0]\"; filename=\"file1.txt\"\r\n";
        $data .= "Content-Type: text/plain\r\n";
        $data .= "\r\n";
        $data .= "File 1 content\r\n";
        $data .= "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"files[1]\"; filename=\"file2.txt\"\r\n";
        $data .= "Content-Type: text/plain\r\n";
        $data .= "\r\n";
        $data .= "File 2 content\r\n";
        $data .= "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"avatar\"; filename=\"user.png\"\r\n";
        $data .= "Content-Type: image/png\r\n";
        $data .= "\r\n";
        $data .= "PNG fake content\r\n";
        $data .= "--boundary123--\r\n";

        $stream = $this->createStream($data);
        $files = $this->parser->parseFilesFromStream($stream, $boundary);
        fclose($stream);

        $this->assertArrayHasKey('files', $files);
        $this->assertArrayHasKey('avatar', $files);

        $this->assertIsArray($files['files']);
        $this->assertCount(2, $files['files']);

        $this->assertEquals('file1.txt', $files['files'][0]->name);
        $this->assertEquals('file2.txt', $files['files'][1]->name);
        $this->assertEquals('user.png', $files['avatar']->name);

        // Verify content
        $this->assertEquals("File 1 content\r\n", file_get_contents($files['files'][0]->tmpName));
        $this->assertEquals("File 2 content\r\n", file_get_contents($files['files'][1]->tmpName));
    }

    public function testMaxFileCountLimit(): void
    {
        $config = new ParserConfig(maxFileCount: 2);
        $parser = new StreamingParser($config);
        $boundary = 'boundary123';

        $data = "--boundary123\r\n";
        // Add 3 files to exceed limit
        for ($i = 1; $i <= 3; $i++) {
            $data .= "Content-Disposition: form-data; name=\"file$i\"; filename=\"file$i.txt\"\r\n";
            $data .= "\r\n";
            $data .= "Content $i\r\n";
            $data .= "--boundary123\r\n";
        }
        $data .= "--boundary123--\r\n";

        $stream = $this->createStream($data);

        $this->expectException(\App\Framework\Http\Parser\Exception\ParserSecurityException::class);
        $this->expectExceptionMessage('Maximum number of parts exceeded');

        // Consume all parts to trigger exception
        $parts = iterator_to_array($parser->streamMultipart($stream, $boundary));

        fclose($stream);
    }

    public function testFileSizeLimit(): void
    {
        $config = new ParserConfig(maxFileSize: Byte::fromKilobytes(1)); // 1KB limit
        $parser = new StreamingParser($config);
        $boundary = 'boundary123';

        // Create file larger than 1KB
        $largeContent = str_repeat('X', 2048); // 2KB

        $data = "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"large.txt\"\r\n";
        $data .= "\r\n";
        $data .= $largeContent . "\r\n";
        $data .= "--boundary123--\r\n";

        $stream = $this->createStream($data);

        $this->expectException(\App\Framework\Http\Parser\Exception\ParserSecurityException::class);
        $this->expectExceptionMessage('File size exceeded');

        // Consume parts to trigger exception
        foreach ($parser->streamMultipart($stream, $boundary) as $part) {
            // Exception should be thrown when finalizing the part
        }

        fclose($stream);
    }

    public function testFieldValueLengthLimit(): void
    {
        $config = new ParserConfig(maxFieldValueLength: 10);
        $parser = new StreamingParser($config);
        $boundary = 'boundary123';

        $data = "--boundary123\r\n";
        $data .= "Content-Disposition: form-data; name=\"field\"\r\n";
        $data .= "\r\n";
        $data .= "This value is too long\r\n";
        $data .= "--boundary123--\r\n";

        $stream = $this->createStream($data);

        $this->expectException(\App\Framework\Http\Parser\Exception\ParserSecurityException::class);
        $this->expectExceptionMessage('Field value too long');

        foreach ($parser->streamMultipart($stream, $boundary) as $part) {
            // Exception should be thrown while reading field data
        }

        fclose($stream);
    }

    public function testInvalidStreamResource(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('First parameter must be a valid stream resource');

        // Pass non-resource
        foreach ($this->parser->streamMultipart('not a stream', 'boundary') as $part) {
            // Should throw before yielding
        }
    }

    public function testEmptyStream(): void
    {
        $stream = $this->createStream('');
        $parts = iterator_to_array($this->parser->streamMultipart($stream, 'boundary'));
        fclose($stream);

        $this->assertCount(0, $parts);
    }

    /**
     * Create an in-memory stream from string data
     */
    private function createStream(string $data)
    {
        $stream = fopen('php://memory', 'r+');
        fwrite($stream, $data);
        rewind($stream);

        return $stream;
    }
}
