<?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\PreSaveProcessor;
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\Logging\Logger;
use App\Framework\OAuth\OAuthServiceInterface;
use App\Framework\OAuth\Providers\SupportsPreSaves;
use App\Framework\OAuth\ValueObjects\AccessToken;
use App\Framework\OAuth\ValueObjects\OAuthToken;
use App\Framework\OAuth\ValueObjects\RefreshToken;
use App\Framework\OAuth\ValueObjects\TokenType;
use App\Framework\OAuth\Storage\StoredOAuthToken;

// Helper function to create test OAuthToken
function createTestToken(string $accessToken = 'test_access', int $expiresIn = 3600): OAuthToken
{
    return new OAuthToken(
        accessToken: AccessToken::fromProviderResponse($accessToken, $expiresIn),
        refreshToken: RefreshToken::create('test_refresh'),
        tokenType: TokenType::BEARER,
        scope: null
    );
}

// Mock Music Provider for Testing (implements SupportsPreSaves)
class ProcessorMockMusicProvider implements SupportsPreSaves
{
    public array $addedTracks = [];
    public bool $shouldFail = false;

    public function addTracksToLibrary(OAuthToken $token, array $trackIds): bool
    {
        if ($this->shouldFail) {
            throw new \Exception('Music API error');
        }
        foreach ($trackIds as $trackId) {
            $this->addedTracks[] = $trackId;
        }
        return true;
    }
}

// Mock Logger for Testing
class ProcessorMockLogger implements Logger
{
    public array $logs = [];

    public function debug(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
    {
        $this->logs[] = ['level' => 'debug', 'message' => $message, 'context' => $context];
    }

    public function info(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
    {
        $this->logs[] = ['level' => 'info', 'message' => $message, 'context' => $context];
    }

    public function notice(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
    {
        $this->logs[] = ['level' => 'notice', 'message' => $message, 'context' => $context];
    }

    public function warning(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
    {
        $this->logs[] = ['level' => 'warning', 'message' => $message, 'context' => $context];
    }

    public function error(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
    {
        $this->logs[] = ['level' => 'error', 'message' => $message, 'context' => $context];
    }

    public function critical(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
    {
        $this->logs[] = ['level' => 'critical', 'message' => $message, 'context' => $context];
    }

    public function alert(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
    {
        $this->logs[] = ['level' => 'alert', 'message' => $message, 'context' => $context];
    }

    public function emergency(string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
    {
        $this->logs[] = ['level' => 'emergency', 'message' => $message, 'context' => $context];
    }

    public function log(\App\Framework\Logging\LogLevel $level, string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
    {
        $this->logs[] = ['level' => $level->value, 'message' => $message, 'context' => $context];
    }

    public function logToChannel(\App\Framework\Logging\LogChannel $channel, \App\Framework\Logging\LogLevel $level, string $message, ?\App\Framework\Logging\ValueObjects\LogContext $context = null): void
    {
        $this->logs[] = ['channel' => $channel->value, 'level' => $level->value, 'message' => $message, 'context' => $context];
    }

    public \App\Framework\Logging\ChannelLogger $security {
        get {
            static $securityLogger = null;
            return $securityLogger ??= new \App\Framework\Logging\ChannelLogger($this, \App\Framework\Logging\LogChannel::SECURITY);
        }
    }

    public \App\Framework\Logging\ChannelLogger $cache {
        get {
            static $cacheLogger = null;
            return $cacheLogger ??= new \App\Framework\Logging\ChannelLogger($this, \App\Framework\Logging\LogChannel::CACHE);
        }
    }

    public \App\Framework\Logging\ChannelLogger $database {
        get {
            static $dbLogger = null;
            return $dbLogger ??= new \App\Framework\Logging\ChannelLogger($this, \App\Framework\Logging\LogChannel::DATABASE);
        }
    }

    public \App\Framework\Logging\ChannelLogger $framework {
        get {
            static $frameworkLogger = null;
            return $frameworkLogger ??= new \App\Framework\Logging\ChannelLogger($this, \App\Framework\Logging\LogChannel::FRAMEWORK);
        }
    }

    public \App\Framework\Logging\ChannelLogger $error {
        get {
            static $errorLogger = null;
            return $errorLogger ??= new \App\Framework\Logging\ChannelLogger($this, \App\Framework\Logging\LogChannel::ERROR);
        }
    }
}

// Extended Mock Campaign Repository for Processor
class ProcessorMockCampaignRepository implements PreSaveCampaignRepositoryInterface
{
    private array $campaigns = [];
    public array $readyForRelease = [];
    public array $readyForProcessing = [];

    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 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 $this->readyForRelease;
    }

    public function findReadyForProcessing(): array
    {
        return $this->readyForProcessing;
    }

    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,
        ];
    }
}

// Extended Mock Registration Repository for Processor
class ProcessorMockRegistrationRepository implements PreSaveRegistrationRepositoryInterface
{
    private array $registrations = [];
    public array $pendingByCampaign = [];
    public array $retryable = [];

    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 $this->pendingByCampaign[$campaignId] ?? [];
    }

    public function findRetryable(int $campaignId, int $maxRetries): array
    {
        return $this->retryable[$campaignId] ?? [];
    }

    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;
    }
}

// Mock OAuth Service for Processor
class ProcessorMockOAuthService implements OAuthServiceInterface
{
    private array $tokens = [];

    public function addToken(string $userId, StreamingPlatform $platform, OAuthToken $token): void
    {
        $this->tokens[$userId . '_' . $platform->value] = $token;
    }

    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): StoredOAuthToken
    {
        throw new \RuntimeException('Not implemented in test');
    }

    public function getTokenForUser(string $userId, string $provider): StoredOAuthToken
    {
        $key = $userId . '_' . $provider;
        $token = $this->tokens[$key] ?? throw new \RuntimeException("No token for {$userId}/{$provider}");

        return new StoredOAuthToken(
            id: null,
            userId: $userId,
            provider: $provider,
            token: $token,
            createdAt: Timestamp::now(),
            updatedAt: Timestamp::now()
        );
    }

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

    public function revokeToken(string $userId, string $provider): bool
    {
        return true;
    }

    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->tokens[$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('PreSaveProcessor', function () {
    beforeEach(function () {
        $this->campaignRepo = new ProcessorMockCampaignRepository();
        $this->registrationRepo = new ProcessorMockRegistrationRepository();
        $this->oauthService = new ProcessorMockOAuthService();
        $this->musicProvider = new ProcessorMockMusicProvider();
        $this->logger = new ProcessorMockLogger();

        $this->processor = new PreSaveProcessor(
            $this->campaignRepo,
            $this->registrationRepo,
            $this->oauthService,
            $this->musicProvider,
            $this->logger
        );
    });

    describe('processReleasedCampaigns', function () {
        it('marks campaigns as released when release date reached', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Released Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('-1 day')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $campaign->publish();
            $campaign = $this->campaignRepo->save($campaign);

            $this->campaignRepo->readyForRelease = [$campaign];

            $result = $this->processor->processReleasedCampaigns();

            expect(isset($result['campaigns_marked_for_release']))->toBeTrue();
            expect($result['campaigns_marked_for_release'])->toBe(1);
        });

        it('returns zero when no campaigns ready for release', function () {
            $result = $this->processor->processReleasedCampaigns();

            expect($result['campaigns_marked_for_release'])->toBe(0);
        });
    });

    describe('processPendingRegistrations', function () {
        it('processes pending registrations for released campaigns', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Processing Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::now(),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $campaign->publish()->markAsReleased();
            $campaign = $this->campaignRepo->save($campaign);

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

            $token = createTestToken('access_token_user123');
            $this->oauthService->addToken('user123', StreamingPlatform::SPOTIFY, $token);

            $this->campaignRepo->readyForProcessing = [$campaign];
            $this->registrationRepo->pendingByCampaign[$campaign->id] = [$registration];

            $result = $this->processor->processPendingRegistrations();

            expect(isset($result['campaigns_processed']))->toBeTrue();
            expect($result['campaigns_processed'])->toBe(1);
            expect(isset($result['total_processed']))->toBeTrue();
            expect(isset($result['total_successful']))->toBeTrue();
        });

        it('marks campaign as completed when no pending registrations remain', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Complete Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::now(),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $campaign->publish()->markAsReleased();
            $campaign = $this->campaignRepo->save($campaign);

            $this->campaignRepo->readyForProcessing = [$campaign];
            $this->registrationRepo->pendingByCampaign[$campaign->id] = [];

            $result = $this->processor->processPendingRegistrations();

            $updatedCampaign = $this->campaignRepo->findById($campaign->id);
            expect($updatedCampaign->status)->toBe(CampaignStatus::COMPLETED);
        });
    });

    describe('processCampaignRegistrations', function () {
        it('processes all pending registrations for a campaign', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Batch Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::now(),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $campaign->publish()->markAsReleased();
            $campaign = $this->campaignRepo->save($campaign);

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

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

            $token = createTestToken('access_token_users');
            $this->oauthService->addToken('user1', StreamingPlatform::SPOTIFY, $token);
            $this->oauthService->addToken('user2', StreamingPlatform::SPOTIFY, $token);

            $this->registrationRepo->pendingByCampaign[$campaign->id] = [$reg1, $reg2];

            $result = $this->processor->processCampaignRegistrations($campaign);

            expect($result['processed'])->toBe(2);
            expect($result['successful'])->toBe(2);
            expect($result['failed'])->toBe(0);
        });

        it('handles registration processing failures gracefully', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Failure Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::now(),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $campaign->publish()->markAsReleased();
            $campaign = $this->campaignRepo->save($campaign);

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

            $token = createTestToken('access_token_user123');
            $this->oauthService->addToken('user123', StreamingPlatform::SPOTIFY, $token);

            $this->musicProvider->shouldFail = true;
            $this->registrationRepo->pendingByCampaign[$campaign->id] = [$registration];

            $result = $this->processor->processCampaignRegistrations($campaign);

            expect($result['processed'])->toBe(1);
            expect($result['successful'])->toBe(0);
            expect($result['failed'])->toBe(1);

            $hasError = false;
            foreach ($this->logger->logs as $log) {
                if ($log['level'] === 'error' && str_contains($log['message'], 'Failed to process')) {
                    $hasError = true;
                }
            }
            expect($hasError)->toBeTrue();
        });
    });

    describe('retryFailedRegistrations', function () {
        it('retries failed registrations within retry limit', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Retry Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::now(),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $campaign->publish()->markAsReleased();
            $campaign = $this->campaignRepo->save($campaign);

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

            $token = createTestToken('access_token_user123');
            $this->oauthService->addToken('user123', StreamingPlatform::SPOTIFY, $token);

            $this->registrationRepo->retryable[$campaign->id] = [$registration];

            $result = $this->processor->retryFailedRegistrations($campaign->id, 3);

            expect($result['processed'])->toBe(1);
            expect($result['successful'])->toBe(1);
        });

        it('does not retry registrations exceeding max retry count', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Max Retry Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::now(),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'track123')]
            );
            $campaign = $campaign->publish()->markAsReleased();
            $campaign = $this->campaignRepo->save($campaign);

            $this->registrationRepo->retryable[$campaign->id] = [];

            $result = $this->processor->retryFailedRegistrations($campaign->id, 3);

            expect($result['processed'])->toBe(0);
        });
    });

    describe('Music platform integration', function () {
        it('successfully adds tracks to user library', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Music Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::now(),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:test123')]
            );
            $campaign = $campaign->publish()->markAsReleased();
            $campaign = $this->campaignRepo->save($campaign);

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

            $token = createTestToken('access_token_user123');
            $this->oauthService->addToken('user123', StreamingPlatform::SPOTIFY, $token);

            $this->registrationRepo->pendingByCampaign[$campaign->id] = [$registration];

            $this->processor->processCampaignRegistrations($campaign);

            expect($this->musicProvider->addedTracks)->toHaveCount(1);
            expect($this->musicProvider->addedTracks[0])->toBe('spotify:track:test123');

            $hasInfo = false;
            foreach ($this->logger->logs as $log) {
                if ($log['level'] === 'info' && str_contains($log['message'], 'successfully')) {
                    $hasInfo = true;
                }
            }
            expect($hasInfo)->toBeTrue();
        });
    });
});
