<?php

declare(strict_types=1);
namespace Tests\Adawolfa\MemoryStream;
use Adawolfa\MemoryStream\MemoryStreamWrapper;
use PHPUnit\Framework\TestCase;
use FFI;
use function Adawolfa\MemoryStream\memory_open;
use TypeError;
use ReflectionObject;

final class MemoryStreamWrapperTest extends TestCase
{

	public function testOpen(): void
	{
		$data = FFI::new('char');
		$stream = memory_open(FFI::addr($data), 'r', 1);
		$this->assertTrue(is_resource($stream));
	}

	public function testOpenLongAddress(): void
	{
		if (PHP_INT_SIZE === 8) {
			$stream = fopen('ffi.memory://0x8000000000000538;10', 'r');
		} else {
			$stream = fopen('ffi.memory://0x80000539;10', 'r');
		}

		$this->assertTrue(is_resource($stream));

		if (PHP_INT_SIZE === 8) {
			$this->assertSame([0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x38], self::getStreamAddress($stream));
		} else {
			$this->assertSame([0x80, 0x00, 0x05, 0x39], self::getStreamAddress($stream));
		}

		$ptr = FFI::new('void *');

		if (PHP_INT_SIZE === 8) {
			FFI::memcpy(FFI::addr($ptr), "\x38\x05\x00\x00\x00\x00\x00\x80", 8);
		} else {
			FFI::memcpy(FFI::addr($ptr), "\x39\x05\x00\x80", 4);
		}

		$stream = memory_open($ptr, 'r', 10);
		$this->assertTrue(is_resource($stream));

		if (PHP_INT_SIZE === 8) {
			$this->assertSame([0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x38], self::getStreamAddress($stream));
		} else {
			$this->assertSame([0x80, 0x00, 0x05, 0x39], self::getStreamAddress($stream));
		}
	}

	/**
	 * Returns address of the wrapper's pointer (big-endian).
	 */
	private function getStreamAddress(mixed $stream): array
	{
		$wrapper = stream_get_meta_data($stream)['wrapper_data'];
		$reflection    = new ReflectionObject($wrapper);
		$ptrReflection = $reflection->getProperty('ptr');
		$ptrReflection->setAccessible(true);
		$copyPtr = $ptrReflection->getValue($wrapper);

		$addrBytes = FFI::new(FFI::arrayType(FFI::type('unsigned char'), [PHP_INT_SIZE]));
		FFI::memcpy(FFI::addr($addrBytes), FFI::addr($copyPtr), PHP_INT_SIZE);

		$address = [];

		for ($i = 0; $i < PHP_INT_SIZE; $i++) {
			$address[] = $addrBytes[$i];
		}

		return array_reverse($address);
	}

	public function testOpenError(): void
	{
		$this->assertFalse(@fopen('ffi.memory://abc', 'r'));
		$this->assertFalse(@fopen('ffi.memory://0x123;1', 'a+'));

		if (PHP_INT_SIZE === 8) {
			$this->assertFalse(@fopen('ffi.memory://0x10000000000000001;1', 'r'));
		} else {
			$this->assertFalse(@fopen('ffi.memory://0x100000001;1', 'r'));
		}
	}

	public function testRead(): void
	{
		$data = FFI::new('char[10]');
		FFI::memcpy($data, '0123456789', 10);

		$stream = memory_open(FFI::addr($data), 'r', 10);

		$this->assertSame('01234', fread($stream, 5));
		$this->assertSame('56789', fread($stream, 7));
		$this->assertSame('', fread($stream, 2));
		$this->assertTrue(feof($stream));
	}

	public function testReadErrorNotReadable(): void
	{
		$data = FFI::new('char');
		$stream = memory_open(FFI::addr($data), 'w', 1);
		$this->assertFalse(@fread($stream, 1));
	}

	public function testWrite(): void
	{
		$data = FFI::new('char[11]');
		$stream = memory_open(FFI::addr($data), 'w', 11);

		$this->assertSame(5, fwrite($stream, 'hello'));
		$this->assertSame('hello', FFI::string($data, 5));

		$this->assertSame(6, fwrite($stream, ' world'));
		$this->assertSame('hello world', FFI::string($data, 11));

		$this->assertTrue(feof($stream));
	}

	public function testWriteErrorNotWritable(): void
	{
		$data = FFI::new('char');
		$stream = memory_open(FFI::addr($data), 'r', 1);
		$this->assertSame(0, @fwrite($stream, 'a'));
	}

	public function testWriteErrorPastBuffer(): void
	{
		$data = FFI::new('char');
		$stream = memory_open(FFI::addr($data), 'w', 3);
		$this->assertSame(3, @fwrite($stream, 'hello'));
		$this->assertSame(0, @fwrite($stream, 'hello'));
	}

	public function testReadWrite(): void
	{
		$data = FFI::new('char[3]');
		$stream = memory_open(FFI::addr($data), 'rw', 3);
		$this->assertSame(3, fwrite($stream, 'foo'));
		$this->assertSame(0, fseek($stream, 0));
		$this->assertSame('foo', stream_get_contents($stream));
	}

	public function testSeekTell(): void
	{
		$data = FFI::new('char[10]');
		FFI::memcpy($data, '0123456789', 10);

		$stream = memory_open(FFI::addr($data), 'r', 10);
		$this->assertSame(0, ftell($stream));

		$this->assertSame('01234', fread($stream, 5));
		$this->assertSame(5, ftell($stream));

		$this->assertSame('56789', fread($stream, 7));
		$this->assertSame(10, ftell($stream));

		$this->assertSame('', fread($stream, 2));
		$this->assertSame(10, ftell($stream));

		fseek($stream, 5);
		$this->assertSame(5, ftell($stream));
		$this->assertSame('5', fread($stream, 1));

		fseek($stream, 2, SEEK_CUR);
		$this->assertSame(8, ftell($stream));
		$this->assertSame('8', fread($stream, 1));

		fseek($stream, -1, SEEK_END);
		$this->assertSame(9, ftell($stream));
		$this->assertSame('9', fread($stream, 1));
	}

	public function testStat(): void
	{
		$data = FFI::new('char[10]');
		$stream = memory_open(FFI::addr($data), 'r', 10);
		$this->assertSame(10, fstat($stream)['size']);
	}

	public function testClosed(): void
	{
		$this->expectException(TypeError::class);
		$data = FFI::new('char[10]');
		$stream = memory_open(FFI::addr($data), 'r', 10);
		fclose($stream);
		$this->assertFalse(@fread($stream, 10));
	}

	public function testReadZeroSize(): void
	{
		$data = FFI::new('char[1]');

		$stream = memory_open(FFI::addr($data), 'r', 0);

		$this->assertTrue(feof($stream));
		$this->assertSame('', fread($stream, 1));
	}

	public function testDestruct(): void
	{
		$result = [];

		(function() use(&$result): void {

			$data   = FFI::new('char[1]');
			$stream = memory_open(FFI::addr($data), 'r', 0);
			$wrapper = stream_get_meta_data($stream)['wrapper_data'];
			assert($wrapper instanceof MemoryStreamWrapper);
			$wrapper->destruct = static function() use(&$result): void {
				$result[] = 1;
			};

			$result[] = 0;

		})();

		$this->assertSame([0, 1], $result);
	}

}