<?php

declare(strict_types=1);

use App\Framework\Core\ValueObjects\Byte;
use App\Framework\LiveComponents\ValueObjects\ChunkHash;
use App\Framework\LiveComponents\ValueObjects\ChunkMetadata;
use App\Framework\LiveComponents\ValueObjects\QuarantineStatus;
use App\Framework\LiveComponents\ValueObjects\UploadSession;
use App\Framework\LiveComponents\ValueObjects\UploadSessionId;

describe('UploadSession Value Object', function () {
    it('creates new upload session', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $totalSize = Byte::fromMegabytes(10);

        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'file-uploader-1',
            fileName: 'test.pdf',
            totalSize: $totalSize,
            totalChunks: 10
        );

        expect($session->sessionId)->toBe($sessionId);
        expect($session->componentId)->toBe('file-uploader-1');
        expect($session->fileName)->toBe('test.pdf');
        expect($session->totalSize)->toBe($totalSize);
        expect($session->totalChunks)->toBe(10);
        expect($session->chunks)->toBe([]);
        expect($session->expectedFileHash)->toBeNull();
        expect($session->quarantineStatus)->toBe(QuarantineStatus::PENDING);
    });

    it('sets default timestamps on creation', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $beforeCreate = new DateTimeImmutable();

        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'file-uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10
        );

        $afterCreate = new DateTimeImmutable();

        expect($session->createdAt)->toBeInstanceOf(DateTimeImmutable::class);
        expect($session->createdAt >= $beforeCreate)->toBeTrue();
        expect($session->createdAt <= $afterCreate)->toBeTrue();
        expect($session->completedAt)->toBeNull();
    });

    it('sets default expiry to 24 hours', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));

        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'file-uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10
        );

        $expectedExpiry = $session->createdAt->modify('+24 hours');
        $actualExpiry = $session->expiresAt;

        expect($actualExpiry->getTimestamp())->toBe($expectedExpiry->getTimestamp());
    });

    it('accepts optional expected file hash', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $expectedHash = ChunkHash::fromData('expected final file content');

        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'file-uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10,
            expectedFileHash: $expectedHash
        );

        expect($session->expectedFileHash)->toBe($expectedHash);
    });

    it('rejects empty component id', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));

        expect(fn() => UploadSession::create(
            sessionId: $sessionId,
            componentId: '',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10
        ))->toThrow(InvalidArgumentException::class);
    });

    it('rejects empty file name', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));

        expect(fn() => UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: '',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10
        ))->toThrow(InvalidArgumentException::class);
    });

    it('rejects total chunks less than 1', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));

        expect(fn() => UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 0
        ))->toThrow(InvalidArgumentException::class);
    });

    it('adds chunk to session', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10
        );

        $chunk = ChunkMetadata::create(
            index: 0,
            size: Byte::fromMegabytes(1),
            hash: ChunkHash::fromData('chunk 0 data')
        )->withUploaded();

        $updatedSession = $session->withChunk($chunk);

        expect($updatedSession->chunks)->toHaveCount(1);
        expect($updatedSession->chunks[0])->toBe($chunk);
        expect($session->chunks)->toHaveCount(0); // Original unchanged
    });

    it('sorts chunks by index when adding', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10
        );

        // Add chunks out of order
        $chunk2 = ChunkMetadata::create(2, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 2'));
        $chunk0 = ChunkMetadata::create(0, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 0'));
        $chunk1 = ChunkMetadata::create(1, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 1'));

        $session = $session->withChunk($chunk2);
        $session = $session->withChunk($chunk0);
        $session = $session->withChunk($chunk1);

        expect($session->chunks[0]->index)->toBe(0);
        expect($session->chunks[1]->index)->toBe(1);
        expect($session->chunks[2]->index)->toBe(2);
    });

    it('replaces existing chunk at same index', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10
        );

        $chunk1 = ChunkMetadata::create(0, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 0 v1'));
        $chunk2 = ChunkMetadata::create(0, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 0 v2'));

        $session = $session->withChunk($chunk1);
        expect($session->chunks)->toHaveCount(1);

        $session = $session->withChunk($chunk2);
        expect($session->chunks)->toHaveCount(1);
        expect($session->chunks[0])->toBe($chunk2);
    });

    it('calculates uploaded chunks correctly', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 3
        );

        $uploaded1 = ChunkMetadata::create(0, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 0'))->withUploaded();
        $pending = ChunkMetadata::create(1, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 1'));
        $uploaded2 = ChunkMetadata::create(2, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 2'))->withUploaded();

        $session = $session->withChunk($uploaded1);
        $session = $session->withChunk($pending);
        $session = $session->withChunk($uploaded2);

        $uploadedChunks = $session->getUploadedChunks();
        expect($uploadedChunks)->toHaveCount(2);
        expect($uploadedChunks[0]->index)->toBe(0);
        expect($uploadedChunks[1]->index)->toBe(2);
    });

    it('calculates uploaded bytes correctly', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 3
        );

        $chunk1 = ChunkMetadata::create(0, Byte::fromMegabytes(2), ChunkHash::fromData('chunk 0'))->withUploaded();
        $chunk2 = ChunkMetadata::create(1, Byte::fromMegabytes(3), ChunkHash::fromData('chunk 1'))->withUploaded();

        $session = $session->withChunk($chunk1);
        $session = $session->withChunk($chunk2);

        $uploadedBytes = $session->getUploadedBytes();
        expect($uploadedBytes->toMegabytes())->toBe(5.0); // 2MB + 3MB
    });

    it('calculates progress percentage correctly', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 4
        );

        // Upload 2.5MB of 10MB = 25%
        $chunk1 = ChunkMetadata::create(0, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 0'))->withUploaded();
        $chunk2 = ChunkMetadata::create(1, Byte::fromBytes(1572864), ChunkHash::fromData('chunk 1'))->withUploaded(); // 1.5MB

        $session = $session->withChunk($chunk1);
        $session = $session->withChunk($chunk2);

        expect($session->getProgress())->toBe(25.0);
    });

    it('returns zero progress for empty total size', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'empty.txt',
            totalSize: Byte::fromBytes(0),
            totalChunks: 1
        );

        expect($session->getProgress())->toBe(0.0);
    });

    it('detects completed upload', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(3),
            totalChunks: 3
        );

        expect($session->isComplete())->toBeFalse();

        $chunk0 = ChunkMetadata::create(0, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 0'))->withUploaded();
        $chunk1 = ChunkMetadata::create(1, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 1'))->withUploaded();
        $chunk2 = ChunkMetadata::create(2, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 2'))->withUploaded();

        $session = $session->withChunk($chunk0);
        $session = $session->withChunk($chunk1);
        expect($session->isComplete())->toBeFalse();

        $session = $session->withChunk($chunk2);
        expect($session->isComplete())->toBeTrue();
    });

    it('detects expired session', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $pastExpiry = (new DateTimeImmutable())->modify('-1 hour');

        $session = new UploadSession(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10,
            expiresAt: $pastExpiry
        );

        expect($session->isExpired())->toBeTrue();
    });

    it('detects non-expired session', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $futureExpiry = (new DateTimeImmutable())->modify('+1 hour');

        $session = new UploadSession(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10,
            expiresAt: $futureExpiry
        );

        expect($session->isExpired())->toBeFalse();
    });

    it('identifies missing chunk indices', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(5),
            totalChunks: 5
        );

        // Upload chunks 0, 2, 4 (missing 1 and 3)
        $chunk0 = ChunkMetadata::create(0, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 0'))->withUploaded();
        $chunk2 = ChunkMetadata::create(2, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 2'))->withUploaded();
        $chunk4 = ChunkMetadata::create(4, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 4'))->withUploaded();

        $session = $session->withChunk($chunk0);
        $session = $session->withChunk($chunk2);
        $session = $session->withChunk($chunk4);

        $missing = $session->getMissingChunkIndices();
        expect($missing)->toBe([1, 3]);
    });

    it('returns empty array when all chunks uploaded', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(2),
            totalChunks: 2
        );

        $chunk0 = ChunkMetadata::create(0, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 0'))->withUploaded();
        $chunk1 = ChunkMetadata::create(1, Byte::fromMegabytes(1), ChunkHash::fromData('chunk 1'))->withUploaded();

        $session = $session->withChunk($chunk0);
        $session = $session->withChunk($chunk1);

        expect($session->getMissingChunkIndices())->toBe([]);
    });

    it('updates quarantine status with valid transition', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10
        );

        expect($session->quarantineStatus)->toBe(QuarantineStatus::PENDING);

        $scanning = $session->withQuarantineStatus(QuarantineStatus::SCANNING);
        expect($scanning->quarantineStatus)->toBe(QuarantineStatus::SCANNING);

        $approved = $scanning->withQuarantineStatus(QuarantineStatus::APPROVED);
        expect($approved->quarantineStatus)->toBe(QuarantineStatus::APPROVED);
    });

    it('rejects invalid quarantine status transition', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10
        );

        // Cannot transition directly from PENDING to APPROVED
        expect(fn() => $session->withQuarantineStatus(QuarantineStatus::APPROVED))
            ->toThrow(InvalidArgumentException::class);
    });

    it('marks session as completed', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 10
        );

        expect($session->completedAt)->toBeNull();

        $beforeComplete = new DateTimeImmutable();
        $completed = $session->withCompleted();
        $afterComplete = new DateTimeImmutable();

        expect($completed->completedAt)->toBeInstanceOf(DateTimeImmutable::class);
        expect($completed->completedAt >= $beforeComplete)->toBeTrue();
        expect($completed->completedAt <= $afterComplete)->toBeTrue();
    });

    it('converts to array with all fields', function () {
        $sessionId = UploadSessionId::fromString(str_repeat('a', 32));
        $session = UploadSession::create(
            sessionId: $sessionId,
            componentId: 'uploader-1',
            fileName: 'test.pdf',
            totalSize: Byte::fromMegabytes(10),
            totalChunks: 5
        );

        $chunk0 = ChunkMetadata::create(0, Byte::fromMegabytes(2), ChunkHash::fromData('chunk 0'))->withUploaded();
        $session = $session->withChunk($chunk0);

        $array = $session->toArray();

        expect($array['session_id'])->toBe($sessionId->toString());
        expect($array['component_id'])->toBe('uploader-1');
        expect($array['file_name'])->toBe('test.pdf');
        expect($array['total_size'])->toBe(10485760); // 10MB in bytes
        expect($array['total_size_human'])->toBeString();
        expect($array['total_chunks'])->toBe(5);
        expect($array['uploaded_chunks'])->toBe(1);
        expect($array['uploaded_bytes'])->toBe(2097152); // 2MB in bytes
        expect($array['progress'])->toBe(20.0); // 2MB / 10MB
        expect($array['is_complete'])->toBeFalse();
        expect($array['quarantine_status'])->toBe('pending');
        expect($array)->toHaveKey('created_at');
        expect($array['completed_at'])->toBeNull();
        expect($array)->toHaveKey('expires_at');
        expect($array['is_expired'])->toBeFalse();
        expect($array['missing_chunks'])->toBe([1, 2, 3, 4]);
    });
});
