<?php

declare(strict_types=1);

use App\Domain\PreSave\PreSaveCampaign;
use App\Domain\PreSave\PreSaveCampaignRepositoryInterface;
use App\Domain\PreSave\PreSaveRegistration;
use App\Domain\PreSave\PreSaveRegistrationRepositoryInterface;
use App\Domain\PreSave\Services\PreSaveCampaignService;
use App\Domain\PreSave\ValueObjects\CampaignStatus;
use App\Domain\PreSave\ValueObjects\RegistrationStatus;
use App\Domain\PreSave\ValueObjects\StreamingPlatform;
use App\Domain\PreSave\ValueObjects\TrackUrl;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Exception\FrameworkException;
use App\Framework\OAuth\OAuthServiceInterface;
use App\Framework\OAuth\ValueObjects\OAuthToken;

// In-Memory Campaign Repository for Testing
class InMemoryCampaignRepository implements PreSaveCampaignRepositoryInterface
{
    private array $campaigns = [];

    public function addCampaign(PreSaveCampaign $campaign): void
    {
        $this->campaigns[$campaign->id ?? count($this->campaigns) + 1] = $campaign;
    }

    public function findById(int $id): ?PreSaveCampaign
    {
        return $this->campaigns[$id] ?? null;
    }

    public function findAll(array $filters = []): array
    {
        return array_values($this->campaigns);
    }

    public function findReadyForRelease(): array
    {
        return [];
    }

    public function findReadyForProcessing(): array
    {
        return [];
    }

    public function save(PreSaveCampaign $campaign): PreSaveCampaign
    {
        $id = $campaign->id ?? count($this->campaigns) + 1;
        $saved = new PreSaveCampaign(
            id: $id,
            status: $campaign->status,
            title: $campaign->title,
            artistName: $campaign->artistName,
            coverImageUrl: $campaign->coverImageUrl,
            releaseDate: $campaign->releaseDate,
            trackUrls: $campaign->trackUrls,
            description: $campaign->description,
            startDate: $campaign->startDate,
            createdAt: $campaign->createdAt,
            updatedAt: $campaign->updatedAt
        );
        $this->campaigns[$id] = $saved;
        return $saved;
    }

    public function delete(int $id): bool
    {
        unset($this->campaigns[$id]);
        return true;
    }

    public function getStatistics(int $campaignId): array
    {
        return [
            'total_registrations' => 10,
            'completed' => 5,
            'pending' => 3,
            'failed' => 2,
        ];
    }
}

// In-Memory Registration Repository for Testing
class InMemoryRegistrationRepository implements PreSaveRegistrationRepositoryInterface
{
    private array $registrations = [];

    public function addRegistration(PreSaveRegistration $registration): void
    {
        $this->registrations[] = $registration;
    }

    public function save(PreSaveRegistration $registration): PreSaveRegistration
    {
        $id = $registration->id ?? count($this->registrations) + 1;
        $saved = new PreSaveRegistration(
            id: $id,
            campaignId: $registration->campaignId,
            userId: $registration->userId,
            platform: $registration->platform,
            status: $registration->status,
            registeredAt: $registration->registeredAt,
            processedAt: $registration->processedAt,
            errorMessage: $registration->errorMessage,
            retryCount: $registration->retryCount
        );
        $this->registrations[$id] = $saved;
        return $saved;
    }

    public function findForUserAndCampaign(string $userId, int $campaignId, StreamingPlatform $platform): ?PreSaveRegistration
    {
        foreach ($this->registrations as $reg) {
            if ($reg->userId === $userId && $reg->campaignId === $campaignId && $reg->platform === $platform) {
                return $reg;
            }
        }
        return null;
    }

    public function findByUserId(string $userId): array
    {
        return array_values(array_filter($this->registrations, fn($r) => $r->userId === $userId));
    }

    public function findPendingByCampaign(int $campaignId): array
    {
        return [];
    }

    public function findRetryable(int $campaignId, int $maxRetries): array
    {
        return [];
    }

    public function getStatusCounts(int $campaignId): array
    {
        return [
            'pending' => 3,
            'completed' => 5,
            'failed' => 2,
        ];
    }

    public function delete(int $id): bool
    {
        unset($this->registrations[$id]);
        return true;
    }

    public function hasRegistered(string $userId, int $campaignId, StreamingPlatform $platform): bool
    {
        return $this->findForUserAndCampaign($userId, $campaignId, $platform) !== null;
    }
}

// In-Memory OAuth Service for Testing
class InMemoryOAuthService implements OAuthServiceInterface
{
    private array $providers = [];

    public function addProvider(string $userId, string $provider): void
    {
        $this->providers[$userId . '_' . $provider] = true;
    }

    public function getProvider(string $name): \App\Framework\OAuth\OAuthProvider
    {
        throw new \RuntimeException('Not implemented in test');
    }

    public function getAuthorizationUrl(string $provider, array $options = []): string
    {
        throw new \RuntimeException('Not implemented in test');
    }

    public function handleCallback(string $userId, string $provider, string $code, ?string $state = null): \App\Framework\OAuth\Storage\StoredOAuthToken
    {
        throw new \RuntimeException('Not implemented in test');
    }

    public function getTokenForUser(string $userId, string $provider): \App\Framework\OAuth\Storage\StoredOAuthToken
    {
        throw new \RuntimeException('Not implemented in test');
    }

    public function refreshToken(\App\Framework\OAuth\Storage\StoredOAuthToken $storedToken): \App\Framework\OAuth\Storage\StoredOAuthToken
    {
        throw new \RuntimeException('Not implemented in test');
    }

    public function revokeToken(string $userId, string $provider): bool
    {
        throw new \RuntimeException('Not implemented in test');
    }

    public function getUserProfile(string $userId, string $provider): array
    {
        throw new \RuntimeException('Not implemented in test');
    }

    public function hasProvider(string $userId, string $provider): bool
    {
        return isset($this->providers[$userId . '_' . $provider]);
    }

    public function getUserProviders(string $userId): array
    {
        throw new \RuntimeException('Not implemented in test');
    }

    public function refreshExpiringTokens(int $withinSeconds = 300): int
    {
        throw new \RuntimeException('Not implemented in test');
    }

    public function cleanupExpiredTokens(): int
    {
        throw new \RuntimeException('Not implemented in test');
    }
}

describe('PreSaveCampaignService', function () {
    beforeEach(function () {
        $this->campaignRepo = new InMemoryCampaignRepository();
        $this->registrationRepo = new InMemoryRegistrationRepository();
        $this->oauthService = new InMemoryOAuthService();

        $this->service = new PreSaveCampaignService(
            $this->campaignRepo,
            $this->registrationRepo,
            $this->oauthService
        );
    });

    describe('registerUser', function () {
        it('successfully registers user for campaign', function () {
            $campaign = PreSaveCampaign::create(
                title: 'New Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            $this->oauthService->addProvider('user123', StreamingPlatform::SPOTIFY->getOAuthProvider());

            $registration = $this->service->registerUser('user123', $campaign->id, StreamingPlatform::SPOTIFY);

            expect($registration->userId)->toBe('user123');
            expect($registration->campaignId)->toBe($campaign->id);
            expect($registration->platform)->toBe(StreamingPlatform::SPOTIFY);
            expect($registration->status)->toBe(RegistrationStatus::PENDING);
        });

        it('throws exception when campaign not found', function () {
            expect(fn() => $this->service->registerUser('user123', 999, StreamingPlatform::SPOTIFY))
                ->toThrow(FrameworkException::class);
        })->throws(FrameworkException::class);

        it('throws exception when campaign not accepting registrations', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Old Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('-7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish()->markAsCompleted());

            expect(fn() => $this->service->registerUser('user123', $campaign->id, StreamingPlatform::SPOTIFY))
                ->toThrow(FrameworkException::class);
        })->throws(FrameworkException::class);

        it('throws exception when platform not supported', function () {
            $campaign = PreSaveCampaign::create(
                title: 'New Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            expect(fn() => $this->service->registerUser('user123', $campaign->id, StreamingPlatform::TIDAL))
                ->toThrow(FrameworkException::class);
        })->throws(FrameworkException::class);

        it('throws exception when user already registered', function () {
            $campaign = PreSaveCampaign::create(
                title: 'New Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            $this->oauthService->addProvider('user123', StreamingPlatform::SPOTIFY->getOAuthProvider());

            $existingReg = PreSaveRegistration::create($campaign->id, 'user123', StreamingPlatform::SPOTIFY);
            $this->registrationRepo->addRegistration($existingReg);

            expect(fn() => $this->service->registerUser('user123', $campaign->id, StreamingPlatform::SPOTIFY))
                ->toThrow(FrameworkException::class);
        })->throws(FrameworkException::class);

        it('throws exception when no OAuth token exists', function () {
            $campaign = PreSaveCampaign::create(
                title: 'New Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            expect(fn() => $this->service->registerUser('user123', $campaign->id, StreamingPlatform::SPOTIFY))
                ->toThrow(FrameworkException::class);
        })->throws(FrameworkException::class);
    });

    describe('cancelRegistration', function () {
        it('successfully cancels pending registration', function () {
            $campaign = PreSaveCampaign::create(
                title: 'New Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            $registration = PreSaveRegistration::create($campaign->id, 'user123', StreamingPlatform::SPOTIFY);
            $registration = $this->registrationRepo->save($registration);

            $result = $this->service->cancelRegistration('user123', $campaign->id, StreamingPlatform::SPOTIFY);

            expect($result)->toBeTrue();
        });

        it('returns false when registration not found', function () {
            $result = $this->service->cancelRegistration('user123', 999, StreamingPlatform::SPOTIFY);

            expect($result)->toBeFalse();
        });

        it('throws exception when trying to cancel completed registration', function () {
            $campaign = PreSaveCampaign::create(
                title: 'New Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            $registration = PreSaveRegistration::create($campaign->id, 'user123', StreamingPlatform::SPOTIFY);
            $registration = $registration->markAsCompleted();
            $this->registrationRepo->addRegistration($registration);

            expect(fn() => $this->service->cancelRegistration('user123', $campaign->id, StreamingPlatform::SPOTIFY))
                ->toThrow(FrameworkException::class);
        })->throws(FrameworkException::class);
    });

    describe('getCampaignStats', function () {
        it('returns comprehensive campaign statistics', function () {
            $campaign = PreSaveCampaign::create(
                title: 'New Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            $stats = $this->service->getCampaignStats($campaign->id);

            expect($stats)->toBeArray();
            expect(isset($stats['campaign']))->toBeTrue();
            expect(isset($stats['total_registrations']))->toBeTrue();
            expect(isset($stats['completed']))->toBeTrue();
            expect(isset($stats['pending']))->toBeTrue();
            expect(isset($stats['failed']))->toBeTrue();
            expect(isset($stats['status_breakdown']))->toBeTrue();
            expect(isset($stats['days_until_release']))->toBeTrue();
        });

        it('throws exception when campaign not found', function () {
            expect(fn() => $this->service->getCampaignStats(999))
                ->toThrow(FrameworkException::class);
        })->throws(FrameworkException::class);
    });

    describe('canUserRegister', function () {
        it('returns true when user can register', function () {
            $campaign = PreSaveCampaign::create(
                title: 'New Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            $this->oauthService->addProvider('user123', StreamingPlatform::SPOTIFY->getOAuthProvider());

            $canRegister = $this->service->canUserRegister('user123', $campaign->id, StreamingPlatform::SPOTIFY);

            expect($canRegister)->toBeTrue();
        });

        it('returns false when campaign not found', function () {
            $canRegister = $this->service->canUserRegister('user123', 999, StreamingPlatform::SPOTIFY);

            expect($canRegister)->toBeFalse();
        });

        it('returns false when campaign not accepting registrations', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Old Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('-7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish()->markAsCompleted());

            $canRegister = $this->service->canUserRegister('user123', $campaign->id, StreamingPlatform::SPOTIFY);

            expect($canRegister)->toBeFalse();
        });

        it('returns false when user already registered', function () {
            $campaign = PreSaveCampaign::create(
                title: 'New Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            $this->oauthService->addProvider('user123', StreamingPlatform::SPOTIFY->getOAuthProvider());

            $reg = PreSaveRegistration::create($campaign->id, 'user123', StreamingPlatform::SPOTIFY);
            $this->registrationRepo->addRegistration($reg);

            $canRegister = $this->service->canUserRegister('user123', $campaign->id, StreamingPlatform::SPOTIFY);

            expect($canRegister)->toBeFalse();
        });
    });
});
