<?php

declare(strict_types=1);

use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Filesystem\InMemoryStorage;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Responses\JsonResponse;
use App\Framework\LiveComponents\Controllers\ChunkedUploadController;
use App\Framework\LiveComponents\Services\ChunkAssembler;
use App\Framework\LiveComponents\Services\ChunkedUploadManager;
use App\Framework\LiveComponents\Services\IntegrityValidator;
use App\Framework\LiveComponents\Services\UploadProgressTracker;
use App\Framework\LiveComponents\Services\UploadSessionIdGenerator;
use App\Framework\LiveComponents\Services\UploadSessionStore;
use App\Framework\LiveComponents\ValueObjects\ChunkHash;
use App\Framework\LiveComponents\ValueObjects\UploadSessionId;
use App\Framework\Router\Result\Status;

beforeEach(function () {
    // Setup dependencies
    $this->sessionIdGenerator = new UploadSessionIdGenerator();
    $this->sessionStore = new UploadSessionStore();
    $this->integrityValidator = new IntegrityValidator();
    $this->chunkAssembler = new ChunkAssembler();
    $this->fileStorage = new InMemoryStorage();

    // Mock progress tracker (no SSE in tests)
    $this->progressTracker = new class {
        public function broadcastInitialized($session, $userId): void {}
        public function broadcastChunkUploaded($session, $userId): void {}
        public function broadcastCompleted($session, $userId): void {}
        public function broadcastAborted($sessionId, $userId, $reason): void {}
        public function getProgress($sessionId): ?array { return null; }
    };

    $this->uploadManager = new ChunkedUploadManager(
        $this->sessionIdGenerator,
        $this->sessionStore,
        $this->integrityValidator,
        $this->chunkAssembler,
        $this->fileStorage,
        $this->progressTracker,
        '/tmp/test-uploads'
    );

    $this->controller = new ChunkedUploadController(
        $this->uploadManager,
        $this->progressTracker
    );
});

test('initializes upload session successfully', function () {
    // Arrange
    $request = Mockery::mock(HttpRequest::class);
    $request->parsedBody = (object) [
        'componentId' => 'test-uploader',
        'fileName' => 'test-file.pdf',
        'totalSize' => 1024 * 1024, // 1MB
        'chunkSize' => 512 * 1024,   // 512KB
    ];
    $request->shouldReceive('parsedBody->toArray')
        ->andReturn([
            'componentId' => 'test-uploader',
            'fileName' => 'test-file.pdf',
            'totalSize' => 1024 * 1024,
            'chunkSize' => 512 * 1024,
        ]);

    // Act
    $response = $this->controller->initialize($request);

    // Assert
    expect($response)->toBeInstanceOf(JsonResponse::class);
    expect($response->status)->toBe(Status::OK);

    $data = $response->data;
    expect($data['success'])->toBeTrue();
    expect($data['session_id'])->toBeString();
    expect($data['total_chunks'])->toBe(2); // 1MB / 512KB = 2 chunks
    expect($data['expires_at'])->toBeString();
});

test('returns error when required fields are missing', function () {
    // Arrange
    $request = Mockery::mock(HttpRequest::class);
    $request->parsedBody = (object) [
        'componentId' => 'test-uploader',
        // Missing fileName, totalSize, chunkSize
    ];
    $request->shouldReceive('parsedBody->toArray')
        ->andReturn(['componentId' => 'test-uploader']);

    // Act
    $response = $this->controller->initialize($request);

    // Assert
    expect($response->status)->toBe(Status::BAD_REQUEST);
    expect($response->data['success'])->toBeFalse();
    expect($response->data['error'])->toContain('Missing required fields');
});

test('uploads chunk successfully', function () {
    // Arrange - Initialize session first
    $initRequest = Mockery::mock(HttpRequest::class);
    $initRequest->shouldReceive('parsedBody->toArray')
        ->andReturn([
            'componentId' => 'test-uploader',
            'fileName' => 'test-chunk.txt',
            'totalSize' => 1024,
            'chunkSize' => 512,
        ]);

    $initResponse = $this->controller->initialize($initRequest);
    $sessionId = $initResponse->data['session_id'];

    // Create temp file for chunk upload
    $chunkData = str_repeat('A', 512);
    $chunkHash = ChunkHash::fromData($chunkData);
    $tempFile = tempnam(sys_get_temp_dir(), 'chunk_');
    file_put_contents($tempFile, $chunkData);

    // Upload chunk request
    $uploadRequest = Mockery::mock(HttpRequest::class);
    $uploadRequest->parsedBody = (object) [];
    $uploadRequest->shouldReceive('parsedBody->get')
        ->with('sessionId')
        ->andReturn($sessionId);
    $uploadRequest->shouldReceive('parsedBody->get')
        ->with('chunkIndex')
        ->andReturn(0);
    $uploadRequest->shouldReceive('parsedBody->get')
        ->with('chunkHash')
        ->andReturn($chunkHash->toString());

    $uploadRequest->uploadedFiles = [
        'chunk' => [
            'tmp_name' => $tempFile,
            'size' => 512,
        ],
    ];

    // Act
    $response = $this->controller->uploadChunk($uploadRequest);

    // Assert
    expect($response)->toBeInstanceOf(JsonResponse::class);
    expect($response->status)->toBe(Status::OK);
    expect($response->data['success'])->toBeTrue();
    expect($response->data['progress'])->toBe(50.0); // 1 of 2 chunks
    expect($response->data['uploaded_chunks'])->toBe(1);
    expect($response->data['total_chunks'])->toBe(2);

    // Cleanup
    @unlink($tempFile);
});

test('rejects chunk upload with invalid hash', function () {
    // Arrange - Initialize session
    $initRequest = Mockery::mock(HttpRequest::class);
    $initRequest->shouldReceive('parsedBody->toArray')
        ->andReturn([
            'componentId' => 'test-uploader',
            'fileName' => 'test.txt',
            'totalSize' => 1024,
            'chunkSize' => 512,
        ]);

    $initResponse = $this->controller->initialize($initRequest);
    $sessionId = $initResponse->data['session_id'];

    // Create chunk with wrong hash
    $chunkData = str_repeat('B', 512);
    $wrongHash = ChunkHash::fromData('different data');
    $tempFile = tempnam(sys_get_temp_dir(), 'chunk_');
    file_put_contents($tempFile, $chunkData);

    // Upload request
    $uploadRequest = Mockery::mock(HttpRequest::class);
    $uploadRequest->parsedBody = (object) [];
    $uploadRequest->shouldReceive('parsedBody->get')
        ->with('sessionId')
        ->andReturn($sessionId);
    $uploadRequest->shouldReceive('parsedBody->get')
        ->with('chunkIndex')
        ->andReturn(0);
    $uploadRequest->shouldReceive('parsedBody->get')
        ->with('chunkHash')
        ->andReturn($wrongHash->toString());

    $uploadRequest->uploadedFiles = [
        'chunk' => [
            'tmp_name' => $tempFile,
            'size' => 512,
        ],
    ];

    // Act
    $response = $this->controller->uploadChunk($uploadRequest);

    // Assert
    expect($response->status)->toBe(Status::BAD_REQUEST);
    expect($response->data['success'])->toBeFalse();
    expect($response->data['error'])->toContain('hash mismatch');

    // Cleanup
    @unlink($tempFile);
});

test('returns error when chunk file is missing', function () {
    // Arrange
    $request = Mockery::mock(HttpRequest::class);
    $request->parsedBody = (object) [];
    $request->shouldReceive('parsedBody->get')
        ->with('sessionId')
        ->andReturn('test-session-id');
    $request->shouldReceive('parsedBody->get')
        ->with('chunkIndex')
        ->andReturn(0);
    $request->shouldReceive('parsedBody->get')
        ->with('chunkHash')
        ->andReturn('somehash');

    $request->uploadedFiles = []; // No chunk file uploaded

    // Act
    $response = $this->controller->uploadChunk($request);

    // Assert
    expect($response->status)->toBe(Status::BAD_REQUEST);
    expect($response->data['success'])->toBeFalse();
    expect($response->data['error'])->toBe('No chunk file uploaded');
});

test('completes upload successfully', function () {
    // Arrange - Initialize and upload all chunks
    $initRequest = Mockery::mock(HttpRequest::class);
    $initRequest->shouldReceive('parsedBody->toArray')
        ->andReturn([
            'componentId' => 'test-uploader',
            'fileName' => 'complete-test.txt',
            'totalSize' => 1024,
            'chunkSize' => 512,
        ]);

    $initResponse = $this->controller->initialize($initRequest);
    $sessionId = $initResponse->data['session_id'];

    // Upload chunk 1
    $chunk1Data = str_repeat('A', 512);
    $chunk1Hash = ChunkHash::fromData($chunk1Data);
    $tempFile1 = tempnam(sys_get_temp_dir(), 'chunk_');
    file_put_contents($tempFile1, $chunk1Data);

    $uploadRequest1 = Mockery::mock(HttpRequest::class);
    $uploadRequest1->parsedBody = (object) [];
    $uploadRequest1->shouldReceive('parsedBody->get')
        ->with('sessionId')
        ->andReturn($sessionId);
    $uploadRequest1->shouldReceive('parsedBody->get')
        ->with('chunkIndex')
        ->andReturn(0);
    $uploadRequest1->shouldReceive('parsedBody->get')
        ->with('chunkHash')
        ->andReturn($chunk1Hash->toString());
    $uploadRequest1->uploadedFiles = ['chunk' => ['tmp_name' => $tempFile1, 'size' => 512]];

    $this->controller->uploadChunk($uploadRequest1);

    // Upload chunk 2
    $chunk2Data = str_repeat('B', 512);
    $chunk2Hash = ChunkHash::fromData($chunk2Data);
    $tempFile2 = tempnam(sys_get_temp_dir(), 'chunk_');
    file_put_contents($tempFile2, $chunk2Data);

    $uploadRequest2 = Mockery::mock(HttpRequest::class);
    $uploadRequest2->parsedBody = (object) [];
    $uploadRequest2->shouldReceive('parsedBody->get')
        ->with('sessionId')
        ->andReturn($sessionId);
    $uploadRequest2->shouldReceive('parsedBody->get')
        ->with('chunkIndex')
        ->andReturn(1);
    $uploadRequest2->shouldReceive('parsedBody->get')
        ->with('chunkHash')
        ->andReturn($chunk2Hash->toString());
    $uploadRequest2->uploadedFiles = ['chunk' => ['tmp_name' => $tempFile2, 'size' => 512]];

    $this->controller->uploadChunk($uploadRequest2);

    // Complete upload
    $completeRequest = Mockery::mock(HttpRequest::class);
    $completeRequest->shouldReceive('parsedBody->toArray')
        ->andReturn([
            'sessionId' => $sessionId,
            'targetPath' => '/tmp/completed-file.txt',
        ]);

    // Act
    $response = $this->controller->complete($completeRequest);

    // Assert
    expect($response)->toBeInstanceOf(JsonResponse::class);
    expect($response->status)->toBe(Status::OK);
    expect($response->data['success'])->toBeTrue();
    expect($response->data['file_path'])->toBe('/tmp/completed-file.txt');
    expect($response->data['completed_at'])->toBeString();

    // Cleanup
    @unlink($tempFile1);
    @unlink($tempFile2);
});

test('returns error when completing with missing chunks', function () {
    // Arrange - Initialize but don't upload all chunks
    $initRequest = Mockery::mock(HttpRequest::class);
    $initRequest->shouldReceive('parsedBody->toArray')
        ->andReturn([
            'componentId' => 'test-uploader',
            'fileName' => 'incomplete.txt',
            'totalSize' => 1024,
            'chunkSize' => 512,
        ]);

    $initResponse = $this->controller->initialize($initRequest);
    $sessionId = $initResponse->data['session_id'];

    // Upload only first chunk (missing second chunk)
    $chunkData = str_repeat('A', 512);
    $chunkHash = ChunkHash::fromData($chunkData);
    $tempFile = tempnam(sys_get_temp_dir(), 'chunk_');
    file_put_contents($tempFile, $chunkData);

    $uploadRequest = Mockery::mock(HttpRequest::class);
    $uploadRequest->parsedBody = (object) [];
    $uploadRequest->shouldReceive('parsedBody->get')
        ->with('sessionId')
        ->andReturn($sessionId);
    $uploadRequest->shouldReceive('parsedBody->get')
        ->with('chunkIndex')
        ->andReturn(0);
    $uploadRequest->shouldReceive('parsedBody->get')
        ->with('chunkHash')
        ->andReturn($chunkHash->toString());
    $uploadRequest->uploadedFiles = ['chunk' => ['tmp_name' => $tempFile, 'size' => 512]];

    $this->controller->uploadChunk($uploadRequest);

    // Try to complete with missing chunks
    $completeRequest = Mockery::mock(HttpRequest::class);
    $completeRequest->shouldReceive('parsedBody->toArray')
        ->andReturn([
            'sessionId' => $sessionId,
            'targetPath' => '/tmp/incomplete-file.txt',
        ]);

    // Act
    $response = $this->controller->complete($completeRequest);

    // Assert
    expect($response->status)->toBe(Status::BAD_REQUEST);
    expect($response->data['success'])->toBeFalse();
    expect($response->data['error'])->toContain('Upload incomplete');

    // Cleanup
    @unlink($tempFile);
});

test('aborts upload successfully', function () {
    // Arrange - Initialize session
    $initRequest = Mockery::mock(HttpRequest::class);
    $initRequest->shouldReceive('parsedBody->toArray')
        ->andReturn([
            'componentId' => 'test-uploader',
            'fileName' => 'abort-test.txt',
            'totalSize' => 1024,
            'chunkSize' => 512,
        ]);

    $initResponse = $this->controller->initialize($initRequest);
    $sessionId = $initResponse->data['session_id'];

    // Abort request
    $abortRequest = Mockery::mock(HttpRequest::class);
    $abortRequest->shouldReceive('parsedBody->toArray')
        ->andReturn([
            'sessionId' => $sessionId,
            'reason' => 'User cancelled upload',
        ]);

    // Act
    $response = $this->controller->abort($abortRequest);

    // Assert
    expect($response)->toBeInstanceOf(JsonResponse::class);
    expect($response->status)->toBe(Status::OK);
    expect($response->data['success'])->toBeTrue();
});

test('returns error when aborting with missing sessionId', function () {
    // Arrange
    $request = Mockery::mock(HttpRequest::class);
    $request->shouldReceive('parsedBody->toArray')
        ->andReturn([]); // Missing sessionId

    // Act
    $response = $this->controller->abort($request);

    // Assert
    expect($response->status)->toBe(Status::BAD_REQUEST);
    expect($response->data['success'])->toBeFalse();
    expect($response->data['error'])->toBe('Missing required field: sessionId');
});

test('returns status for active upload session', function () {
    // Note: This test requires a mock UploadProgressTracker that returns actual data
    // For now, we'll test the controller handles the response correctly

    $sessionId = UploadSessionId::generate()->toString();

    // Create a progress tracker that returns data
    $progressTracker = new class {
        public function broadcastInitialized($session, $userId): void {}
        public function broadcastChunkUploaded($session, $userId): void {}
        public function broadcastCompleted($session, $userId): void {}
        public function broadcastAborted($sessionId, $userId, $reason): void {}

        public function getProgress($sessionId): ?array {
            return [
                'progress' => 50.0,
                'uploaded_chunks' => 1,
                'total_chunks' => 2,
                'uploaded_bytes' => 512,
                'total_bytes' => 1024,
                'phase' => 'uploading',
                'quarantine_status' => 'pending',
            ];
        }
    };

    $controller = new ChunkedUploadController(
        $this->uploadManager,
        $progressTracker
    );

    // Act
    $response = $controller->status($sessionId);

    // Assert
    expect($response)->toBeInstanceOf(JsonResponse::class);
    expect($response->status)->toBe(Status::OK);
    expect($response->data['success'])->toBeTrue();
    expect($response->data['progress'])->toBe(50.0);
    expect($response->data['uploaded_chunks'])->toBe(1);
    expect($response->data['total_chunks'])->toBe(2);
});

test('returns not found when session does not exist', function () {
    $nonExistentSessionId = UploadSessionId::generate()->toString();

    // Act
    $response = $this->controller->status($nonExistentSessionId);

    // Assert
    expect($response->status)->toBe(Status::NOT_FOUND);
    expect($response->data['success'])->toBeFalse();
    expect($response->data['error'])->toBe('Upload session not found');
});

afterEach(function () {
    Mockery::close();
});
