<?php

namespace Velis\Test\Filesystem\Av\Concrete;

use Generator;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Velis\Filesystem\Av\Concrete\AvScanResult;
use Velis\Filesystem\Av\Concrete\ClamAvClient;
use Velis\Filesystem\Av\Contract\ConnectionInterface;
use Velis\Filesystem\Av\Contract\SocketInterface;
use Velis\Filesystem\Av\Contract\FileHandlerInterface;

// phpcs:disable PHPCS_SecurityAudit.BadFunctions.FilesystemFunctions.WarnFilesystem
class ClamAvClientTest extends TestCase
{
    private ConnectionInterface&MockObject $connectionMock;
    private FileHandlerInterface&MockObject $fileHandlerMock;
    private ClamAvClient $clamAvClient;
    private SocketInterface&MockObject $socketMock;

    protected function setUp(): void
    {
        $this->connectionMock = $this->createMock(ConnectionInterface::class);
        $this->fileHandlerMock = $this->createMock(FileHandlerInterface::class);

        $this->socketMock = $this->createMock(SocketInterface::class);
        $this->connectionMock->method('getSocket')->willReturn($this->socketMock);

        $this->clamAvClient = new ClamAvClient(
            connection: $this->connectionMock,
            fileHandler: $this->fileHandlerMock,
        );
    }

    public function testPingReturnsFalseOnNonPongResponse()
    {
        $this->socketMock->method('send')->willReturn(4);
        $this->socketMock->method('recv')->willReturnCallback(function (&$buffer) {
            $buffer = 'PONGn\'t';
            return strlen($buffer);
        });

        $result = $this->clamAvClient->ping();

        $this->assertFalse($result);
    }

    /**
     * @dataProvider scanResponseProvider
     */
    public function testFileScanInStream(string $response, bool $expectedInfectionStatus, string $filePath): void
    {
        // Arrange
        $fileSize = 1024;
        $fileHandle = fopen('php://memory', 'r+');

        // Create read/write streams for socket communication
        $streams = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
        [$readStream, $writeStream] = $streams;

        $this->fileHandlerMock
            ->expects($this->once())
            ->method('processFile')
            ->with($filePath)
            ->willReturn([
                'handle' => $fileHandle,
                'fileSize' => $fileSize,
            ]);

        $this->connectionMock
            ->expects($this->once())
            ->method('getSocket')
            ->willReturn($this->socketMock);

        $this->socketMock
            ->expects($this->once())
            ->method('socketExportStream')
            ->willReturn($readStream);

        // Write the response to the `write stream`
        fwrite($writeStream, $response);

        // Act
        $result = $this->clamAvClient->fileScanInStream($filePath);

        // Assert
        $this->assertInstanceOf(AvScanResult::class, $result);
        $this->assertEquals($expectedInfectionStatus, $result->isInfected());

        // Cleanup
        fclose($fileHandle);
        fclose($writeStream);
    }

    public function scanResponseProvider(): Generator
    {
        yield 'clean file' => [
            'response' => "stream: OK\n",
            'expectedInfectionStatus' => false,
            'filePath' => '/path/to/clean/file.txt',
        ];

        yield 'infected file' => [
            'response' => "stream: Some.Virus.Name FOUND\n",
            'expectedInfectionStatus' => true,
            'filePath' => '/path/to/infected/file.txt',
        ];

        yield 'different virus signature' => [
            'response' => "stream: Malware.Type.123 FOUND\n",
            'expectedInfectionStatus' => true,
            'filePath' => '/path/to/another/infected/file.txt',
        ];

        yield 'multiline response with virus' => [
            'response' => "stream: Multiple.Threats.Found FOUND\nstream: Additional.Info\n",
            'expectedInfectionStatus' => true,
            'filePath' => '/path/to/multiple/threats/file.txt',
        ];
    }
}
