<?php

namespace NoahVet\Reef\Test\B_Functional\Domain\Http\Multipart;

use NoahVet\Reef\Domain\Http\Multipart\ContentDecoder;
use NoahVet\Reef\Domain\Http\Multipart\HeaderParser;
use NoahVet\Reef\Domain\Http\Multipart\HeaderValueParser;
use NoahVet\Reef\Domain\Http\Multipart\MultipartParser;
use NoahVet\Reef\Domain\Http\Multipart\PartExtractor;
use PHPUnit\Framework\TestCase;

class MultipartParserTest extends TestCase
{
    private MultipartParser $parser;

    protected function setUp(): void
    {
        $headerValueParser = new HeaderValueParser();
        $this->parser = new MultipartParser(
            new HeaderParser(),
            new PartExtractor(),
            new ContentDecoder($headerValueParser),
            $headerValueParser,
        );
    }

    public function testInvalidStreamResource(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Input is not a stream');
        $this->parser->parse('invalid stream resource');
    }

    public function testNoBoundaryPart(): void
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage("Can't find boundary in content type");
        $this->parser->parse($this->fixture('no_boundary.txt'));
    }

    public function testNoFirstBoundaryPart(): void
    {
        $this->expectException(\LogicException::class);
        $this->expectExceptionMessage("Can't find multi-part content");
        $this->parser->parse($this->fixture('no_first_boundary.txt'));
    }

    public function testNoLastBoundaryPart(): void
    {
        $this->expectException(\LogicException::class);
        $this->expectExceptionMessage("Can't find multi-part content");
        $this->parser->parse($this->fixture('no_last_boundary.txt'));
    }

    public function testNoMultiPart(): void
    {
        $part = $this->parser->parse($this->fixture('no_multipart.txt'));

        self::assertFalse($part->isMultiPart());
        self::assertEquals('bar', $part->getBody());
        self::assertEquals([
            'user-agent' => 'curl/7.21.2 (x86_64-apple-darwin)',
            'host' => 'localhost:8080',
            'accept' => '*/*',
            'expect' => '100-continue',
            'content-type' => 'text/plain',
        ], $part->getHeaders());
    }

    public function testCantGetBodyForAMultiPartMessage(): void
    {
        $part = $this->parser->parse($this->fixture('simple_multipart.txt'));

        self::assertTrue($part->isMultiPart());

        $this->expectException(\LogicException::class);
        $this->expectExceptionMessage("MultiPart content, there aren't body");
        $part->getBody();
    }

    public function testNoNewLineAtTheEndOfTheParts(): void
    {
        $content = "Content-Type: multipart/related; boundary=delimiter\r\n"
            ."\r\n"
            ."--delimiter\r\n"
            ."Content-Type:mime/type\r\n"
            ."\r\n"
            ."Content\r\n"
            .'--delimiter--';

        $stream = \fopen('php://temp', 'rw');
        \fwrite($stream, $content);

        $part = $this->parser->parse($stream);
        $parts = $part->getParts();
        self::assertEquals('Content', $parts[0]->getBody());
    }

    public function testNoNewLineAtTheEndOfThePartsWhenNewLineIsOneCharacterLong(): void
    {
        $content = "Content-Type: multipart/related; boundary=delimiter\n"
            ."\n"
            ."--delimiter\n"
            ."Content-Type:mime/type\n"
            ."\n"
            ."Content\n"
            .'--delimiter--';

        $stream = \fopen('php://temp', 'rw');
        \fwrite($stream, $content);

        $part = $this->parser->parse($stream);
        $parts = $part->getParts();
        self::assertEquals('Content', $parts[0]->getBody());
    }

    public function testCantGetPartsForANotMultiPartMessage(): void
    {
        $part = $this->parser->parse($this->fixture('no_multipart.txt'));

        self::assertFalse($part->isMultiPart());

        $this->expectException(\LogicException::class);
        $this->expectExceptionMessage("Not MultiPart content, there aren't any parts");
        $part->getParts();
    }

    public function testSimpleMultiPart(): void
    {
        $part = $this->parser->parse($this->fixture('simple_multipart.txt'));

        self::assertTrue($part->isMultiPart());
        self::assertCount(3, $part->getParts());

        $parts = $part->getParts();

        self::assertEquals('image/png', $parts[0]->getMimeType());
        self::assertEquals(
            "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x0a\x49\x44\x41\x54\x78\x9c\x63\x00\x01\x00\x00\x05\x00\x01",
            $parts[0]->getBody(),
        );
        self::assertEquals('bar', $parts[1]->getBody());
        self::assertEquals('rfc', $parts[2]->getBody());
    }

    public function testMultiLineHeader(): void
    {
        $part = $this->parser->parse($this->fixture('simple_multipart.txt'));

        self::assertEquals('line one line two with space line three with tab', $part->getHeader('X-Multi-Line'));
    }

    public function testFilterByName(): void
    {
        $part = $this->parser->parse($this->fixture('simple_multipart.txt'));

        self::assertCount(1, $part->getPartsByName('foo'));
    }

    public function testPartContent(): void
    {
        $part = $this->parser->parse($this->fixture('simple_multipart.txt'));
        $foo = $part->getPartsByName('foo');

        self::assertEquals('bar', $foo[0]->getBody());
    }

    public function testRFC5987Header(): void
    {
        $part = $this->parser->parse($this->fixture('simple_multipart.txt'));
        $parts = $part->getParts();
        $headerParser = new HeaderValueParser();
        $header = $parts[2]->getHeader('Content-Disposition');

        self::assertEquals('£ rates', $headerParser->getOption($header, 'text1'));
        self::assertEquals('£ and € rates', $headerParser->getOption($header, 'text2'));
    }

    public function testHeaderHelpers(): void
    {
        $part = $this->parser->parse($this->fixture('simple_multipart.txt'));
        $parts = $part->getParts();
        $headerParser = new HeaderValueParser();
        $header = $parts[1]->getHeader('Content-Disposition');

        self::assertEquals('form-data', $headerParser->getValue($header));
        self::assertEquals('foo', $headerParser->getOption($header, 'name'));
    }

    public function testInvalidHeadersAreRejected(): void
    {
        $content = "Content-Type: multipart/form-data; boundary=a\n"
            ."\n"
            ."--a\n"
            ."Content-Disposition: form-data; name=\"0\";\n"
            ."Content-Type: ; *=auto''".\str_repeat('a', 10000)
            ."\r\n\r\n\r\n--a--\r\n";

        $stream = \fopen('php://temp', 'rw');
        \fwrite($stream, $content);

        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Malformed header: header value is too long');
        $this->parser->parse($stream);
    }

    public function testFileHelper(): void
    {
        $part = $this->parser->parse($this->fixture('simple_multipart.txt'));
        $parts = $part->getParts();

        self::assertTrue($parts[0]->isFile());
        self::assertEquals('a.png', $parts[0]->getFileName());
    }

    public function testNestedMultiPart(): void
    {
        $part = $this->parser->parse($this->fixture('nested_multipart.txt'));

        self::assertTrue($part->isMultiPart());

        $parts = $part->getParts();

        self::assertTrue($parts[0]->isMultiPart());
    }

    public function testEmailBase64(): void
    {
        $part = $this->parser->parse($this->fixture('email_base64.txt'));

        self::assertTrue($part->isMultiPart());
        self::assertEquals('This is thé subject', $part->getHeader('Subject'));

        $parts = $part->getParts();

        self::assertEquals('This is the content', $parts[0]->getBody());
        self::assertEquals('This is the côntént', $parts[1]->getBody());
    }

    public function testBase64WithOctetStream(): void
    {
        $part = $this->parser->parse($this->fixture('binary_base64.txt'));

        self::assertTrue($part->isMultiPart());

        $parts = $part->getParts();

        self::assertCount(2, $parts);
        self::assertEquals('bar', $parts[0]->getBody());
        self::assertEquals('test.bin', $parts[1]->getFileName());
        self::assertEquals(\hex2bin('6566ffff'), $parts[1]->getBody());
    }

    public function testQuotedPrintable(): void
    {
        $part = $this->parser->parse($this->fixture('quoted_printable.txt'));

        self::assertTrue($part->isMultiPart());
        self::assertEquals('mûltîpàrt', $part->getHeader('Subject'));

        $parts = $part->getParts();

        self::assertEquals('This is the content', $parts[0]->getBody());
        self::assertEquals('This is the côntént', $parts[1]->getBody());
    }

    public function testCapitalized(): void
    {
        $part = $this->parser->parse($this->fixture('capitalized_multipart.txt'));

        self::assertTrue($part->isMultiPart());
    }

    public function testCantGetBodyStreamForAMultiPartMessage(): void
    {
        $part = $this->parser->parse($this->fixture('simple_multipart.txt'));

        self::assertTrue($part->isMultiPart());

        $this->expectException(\LogicException::class);
        $this->expectExceptionMessage("MultiPart content, there aren't body");
        $part->getBodyStream();
    }

    public function testNoMultiPartBodyStream(): void
    {
        $part = $this->parser->parse($this->fixture('no_multipart.txt'));

        self::assertFalse($part->isMultiPart());

        $stream = $part->getBodyStream();
        self::assertIsResource($stream);
        self::assertEquals('bar', \stream_get_contents($stream));
    }

    public function testSimpleMultiPartBodyStream(): void
    {
        $part = $this->parser->parse($this->fixture('simple_multipart.txt'));
        $parts = $part->getParts();

        $stream0 = $parts[0]->getBodyStream();
        self::assertIsResource($stream0);
        self::assertEquals(
            "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x0a\x49\x44\x41\x54\x78\x9c\x63\x00\x01\x00\x00\x05\x00\x01",
            \stream_get_contents($stream0),
        );

        $stream1 = $parts[1]->getBodyStream();
        self::assertIsResource($stream1);
        self::assertEquals('bar', \stream_get_contents($stream1));

        $stream2 = $parts[2]->getBodyStream();
        self::assertIsResource($stream2);
        self::assertEquals('rfc', \stream_get_contents($stream2));
    }

    public function testEmailBase64BodyStream(): void
    {
        $part = $this->parser->parse($this->fixture('email_base64.txt'));
        $parts = $part->getParts();

        $stream0 = $parts[0]->getBodyStream();
        self::assertIsResource($stream0);
        self::assertEquals('This is the content', \stream_get_contents($stream0));

        $stream1 = $parts[1]->getBodyStream();
        self::assertIsResource($stream1);
        self::assertEquals('This is the côntént', \stream_get_contents($stream1));
    }

    public function testBase64WithOctetStreamBodyStream(): void
    {
        $part = $this->parser->parse($this->fixture('binary_base64.txt'));
        $parts = $part->getParts();

        $stream0 = $parts[0]->getBodyStream();
        self::assertIsResource($stream0);
        self::assertEquals('bar', \stream_get_contents($stream0));

        $stream1 = $parts[1]->getBodyStream();
        self::assertIsResource($stream1);
        self::assertEquals(\hex2bin('6566ffff'), \stream_get_contents($stream1));
    }

    public function testQuotedPrintableBodyStream(): void
    {
        $part = $this->parser->parse($this->fixture('quoted_printable.txt'));
        $parts = $part->getParts();

        $stream0 = $parts[0]->getBodyStream();
        self::assertIsResource($stream0);
        self::assertEquals('This is the content', \stream_get_contents($stream0));

        $stream1 = $parts[1]->getBodyStream();
        self::assertIsResource($stream1);
        self::assertEquals('This is the côntént', \stream_get_contents($stream1));
    }

    public function testMissingContentTypeHeader(): void
    {
        $content = "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary\r\n"
            ."\r\n"
            ."------WebKitFormBoundary\r\n"
            ."Content-Disposition: form-data; name=\"title\"\r\n"
            ."\r\n"
            ."Hello World\r\n"
            ."------WebKitFormBoundary\r\n"
            ."Content-Disposition: form-data; name=\"description\"\r\n"
            ."\r\n"
            ."Test description\r\n"
            ."------WebKitFormBoundary\r\n"
            ."Content-Disposition: form-data; name=\"image\"; filename=\"photo.png\"\r\n"
            ."Content-Type: image/png\r\n"
            ."\r\n"
            ."image data\r\n"
            ."------WebKitFormBoundary--\r\n";

        $stream = \fopen('php://temp', 'rw');
        \fwrite($stream, $content);

        $part = $this->parser->parse($stream);
        $parts = $part->getParts();

        self::assertCount(3, $parts);
        self::assertEquals('title', $parts[0]->getName());
        self::assertEquals('Hello World', $parts[0]->getBody());
        self::assertEquals('description', $parts[1]->getName());
        self::assertEquals('Test description', $parts[1]->getBody());
        self::assertEquals('image', $parts[2]->getName());
        self::assertTrue($parts[2]->isFile());
        self::assertEquals('image/png', $parts[2]->getMimeType());
        self::assertEquals('image data', $parts[2]->getBody());
    }

    public function testMissingContentTypeHeaderBodyStream(): void
    {
        $content = "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary\r\n"
            ."\r\n"
            ."------WebKitFormBoundary\r\n"
            ."Content-Disposition: form-data; name=\"title\"\r\n"
            ."\r\n"
            ."Hello World\r\n"
            ."------WebKitFormBoundary\r\n"
            ."Content-Disposition: form-data; name=\"description\"\r\n"
            ."\r\n"
            ."Test description\r\n"
            ."------WebKitFormBoundary\r\n"
            ."Content-Disposition: form-data; name=\"image\"; filename=\"photo.png\"\r\n"
            ."Content-Type: image/png\r\n"
            ."\r\n"
            ."image data\r\n"
            ."------WebKitFormBoundary--\r\n";

        $stream = \fopen('php://temp', 'rw');
        \fwrite($stream, $content);

        $part = $this->parser->parse($stream);
        $parts = $part->getParts();

        $stream0 = $parts[0]->getBodyStream();
        self::assertIsResource($stream0);
        self::assertEquals('Hello World', \stream_get_contents($stream0));

        $stream1 = $parts[1]->getBodyStream();
        self::assertIsResource($stream1);
        self::assertEquals('Test description', \stream_get_contents($stream1));

        $stream2 = $parts[2]->getBodyStream();
        self::assertIsResource($stream2);
        self::assertEquals('image data', \stream_get_contents($stream2));
    }

    public function testBase64PdfNotCorrupted(): void
    {
        $part = $this->parser->parse($this->fixture('binary_base64_pdf.txt'));

        self::assertTrue($part->isMultiPart());

        $parts = $part->getParts();

        self::assertCount(2, $parts);
        self::assertEquals('hello', $parts[0]->getBody());

        $pdfPart = $parts[1];
        self::assertEquals('application/pdf', $pdfPart->getMimeType());
        self::assertEquals('test.pdf', $pdfPart->getFileName());

        $expected = "\xff\xfe\xab\xcd\xef\x00\x01";
        self::assertEquals($expected, $pdfPart->getBody());

        $stream = $pdfPart->getBodyStream();
        self::assertIsResource($stream);
        self::assertEquals($expected, \stream_get_contents($stream));
    }

    private function fixture(string $name): mixed
    {
        return \fopen(__DIR__.'/_data/'.$name, 'r');
    }
}
