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

// In-Memory Campaign Repository for Integration Testing
class IntegrationCampaignRepository implements PreSaveCampaignRepositoryInterface
{
    private array $campaigns = [];
    private int $nextId = 1;

    public function save(PreSaveCampaign $campaign): PreSaveCampaign
    {
        $id = $campaign->id ?? $this->nextId++;
        $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
    {
        $now = Timestamp::now();
        return array_values(array_filter(
            $this->campaigns,
            fn($c) => $c->status === CampaignStatus::SCHEDULED
                   && $c->releaseDate->isBefore($now)
        ));
    }

    public function findReadyForProcessing(): array
    {
        return array_values(array_filter(
            $this->campaigns,
            fn($c) => $c->status === CampaignStatus::RELEASED
        ));
    }

    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 Integration Testing
class IntegrationRegistrationRepository implements PreSaveRegistrationRepositoryInterface
{
    private array $registrations = [];
    private int $nextId = 1;

    public function save(PreSaveRegistration $registration): PreSaveRegistration
    {
        $id = $registration->id ?? $this->nextId++;
        $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 array_values(array_filter(
            $this->registrations,
            fn($r) => $r->campaignId === $campaignId
                   && $r->status === RegistrationStatus::PENDING
        ));
    }

    public function findRetryable(int $campaignId, int $maxRetries): array
    {
        return array_values(array_filter(
            $this->registrations,
            fn($r) => $r->campaignId === $campaignId
                   && $r->status === RegistrationStatus::FAILED
                   && $r->retryCount < $maxRetries
        ));
    }

    public function getStatusCounts(int $campaignId): array
    {
        $counts = ['pending' => 0, 'completed' => 0, 'failed' => 0];
        foreach ($this->registrations as $reg) {
            if ($reg->campaignId === $campaignId) {
                match ($reg->status) {
                    RegistrationStatus::PENDING => $counts['pending']++,
                    RegistrationStatus::COMPLETED => $counts['completed']++,
                    RegistrationStatus::FAILED => $counts['failed']++,
                };
            }
        }
        return $counts;
    }

    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 Integration Testing
class IntegrationOAuthService implements OAuthServiceInterface
{
    private array $providers = [];
    private array $tokens = [];

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

        // Create a token for this provider
        $this->tokens[$userId . '_' . $provider] = new StoredOAuthToken(
            userId: $userId,
            provider: $provider,
            token: new OAuthToken(
                accessToken: 'test_access_token_' . $userId,
                tokenType: 'Bearer',
                expiresIn: 3600,
                refreshToken: 'test_refresh_token_' . $userId,
                scope: ['user-library-modify']
            ),
            createdAt: Timestamp::now(),
            updatedAt: Timestamp::now()
        );
    }

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

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

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

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

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

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

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

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

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

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

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

// Mock Music Provider for Integration Testing (implements SupportsPreSaves)
class IntegrationMusicProvider 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[] = [
                'track_id' => $trackId,
                'token' => $token->accessToken,
                'timestamp' => time()
            ];
        }

        return true;
    }
}

// Mock Logger for Integration Testing
class IntegrationLogger 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);
        }
    }
}

describe('PreSave Integration Tests', function () {
    beforeEach(function () {
        $this->campaignRepo = new IntegrationCampaignRepository();
        $this->registrationRepo = new IntegrationRegistrationRepository();
        $this->oauthService = new IntegrationOAuthService();
        $this->logger = new IntegrationLogger();
        $this->musicProvider = new IntegrationMusicProvider();

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

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

    describe('Full Campaign Lifecycle', function () {
        it('completes full campaign workflow from creation to processing', function () {
            // 1. Create and publish campaign
            $campaign = PreSaveCampaign::create(
                title: 'Integration Test Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:test123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            expect($campaign->id)->not->toBeNull();
            expect($campaign->status)->toBe(CampaignStatus::SCHEDULED);

            // 2. Register multiple users
            $this->oauthService->addProvider('user1', StreamingPlatform::SPOTIFY->getOAuthProvider());
            $this->oauthService->addProvider('user2', StreamingPlatform::SPOTIFY->getOAuthProvider());
            $this->oauthService->addProvider('user3', StreamingPlatform::SPOTIFY->getOAuthProvider());

            $reg1 = $this->campaignService->registerUser('user1', $campaign->id, StreamingPlatform::SPOTIFY);
            $reg2 = $this->campaignService->registerUser('user2', $campaign->id, StreamingPlatform::SPOTIFY);
            $reg3 = $this->campaignService->registerUser('user3', $campaign->id, StreamingPlatform::SPOTIFY);

            expect($reg1->status)->toBe(RegistrationStatus::PENDING);
            expect($reg2->status)->toBe(RegistrationStatus::PENDING);
            expect($reg3->status)->toBe(RegistrationStatus::PENDING);

            // 3. Simulate release date passing
            $releasedCampaign = $campaign->markAsReleased();
            $this->campaignRepo->save($releasedCampaign);

            // 4. Process registrations
            $result = $this->processor->processCampaignRegistrations($releasedCampaign);

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

            // 5. Verify tracks were added to music provider
            expect($this->musicProvider->addedTracks)->toHaveCount(3);
            expect($this->musicProvider->addedTracks[0]['track_id'])->toBe('spotify:track:test123');

            // 6. Verify logging
            $successLogs = array_filter(
                $this->logger->logs,
                fn($log) => $log['level'] === 'info'
                         && str_contains($log['message'], 'successfully')
            );
            expect($successLogs)->toHaveCount(3);

            echo "\n✅ Full campaign lifecycle completed successfully!\n";
            echo "  - Campaign created and published\n";
            echo "  - 3 users registered\n";
            echo "  - Campaign released and processed\n";
            echo "  - All tracks added to user libraries\n";
        });
    });

    describe('User Registration Flow', function () {
        it('prevents duplicate registrations', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Duplicate Test Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:dup123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

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

            // First registration should succeed
            $reg1 = $this->campaignService->registerUser('user1', $campaign->id, StreamingPlatform::SPOTIFY);
            expect($reg1)->not->toBeNull();

            // Second registration should throw exception
            expect(fn() => $this->campaignService->registerUser('user1', $campaign->id, StreamingPlatform::SPOTIFY))
                ->toThrow(FrameworkException::class);
        });

        it('validates OAuth provider exists before registration', function () {
            $campaign = PreSaveCampaign::create(
                title: 'OAuth Test Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:oauth123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            // User without OAuth provider should fail
            expect(fn() => $this->campaignService->registerUser('user_no_oauth', $campaign->id, StreamingPlatform::SPOTIFY))
                ->toThrow(FrameworkException::class);
        });

        it('allows cancellation of pending registrations', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Cancel Test Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:cancel123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

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

            $registration = $this->campaignService->registerUser('user1', $campaign->id, StreamingPlatform::SPOTIFY);
            expect($registration->status)->toBe(RegistrationStatus::PENDING);

            $cancelled = $this->campaignService->cancelRegistration('user1', $campaign->id, StreamingPlatform::SPOTIFY);
            expect($cancelled)->toBeTrue();

            // Should be able to register again after cancellation
            $newRegistration = $this->campaignService->registerUser('user1', $campaign->id, StreamingPlatform::SPOTIFY);
            expect($newRegistration->status)->toBe(RegistrationStatus::PENDING);
        });
    });

    describe('Campaign Processing Flow', function () {
        it('marks campaigns as released when release date passes', function () {
            // Create campaign with past release date
            $pastDate = Timestamp::fromDateTime(new \DateTimeImmutable('-1 day'));
            $campaign = PreSaveCampaign::create(
                title: 'Past Release Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: $pastDate,
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:past123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            // Find campaigns ready for release
            $readyForRelease = $this->campaignRepo->findReadyForRelease();
            expect($readyForRelease)->toHaveCount(1);
            expect($readyForRelease[0]->id)->toBe($campaign->id);

            // Mark as released
            $releasedCampaign = $campaign->markAsReleased();
            $this->campaignRepo->save($releasedCampaign);

            // Should now appear in ready for processing
            $readyForProcessing = $this->campaignRepo->findReadyForProcessing();
            expect($readyForProcessing)->toHaveCount(1);
            expect($readyForProcessing[0]->status)->toBe(CampaignStatus::RELEASED);
        });

        it('handles processing errors gracefully', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Error Test Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('-1 day')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:error123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish()->markAsReleased());

            // Register user but don't provide OAuth token
            $registration = PreSaveRegistration::create(
                $campaign->id,
                'user_without_token',
                StreamingPlatform::SPOTIFY
            );
            $this->registrationRepo->save($registration);

            // Processing should handle error
            $result = $this->processor->processCampaignRegistrations($campaign);

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

            // Verify error was logged
            $errorLogs = array_filter(
                $this->logger->logs,
                fn($log) => $log['level'] === 'error'
            );
            expect($errorLogs)->not->toBeEmpty();
        });

        it('marks campaign as completed when all registrations processed', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Complete Test Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('-1 day')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:complete123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish()->markAsReleased());

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

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

            // Process all registrations
            $result = $this->processor->processCampaignRegistrations($campaign);
            expect($result['successful'])->toBe(1);

            // No more pending registrations
            $pending = $this->registrationRepo->findPendingByCampaign($campaign->id);
            expect($pending)->toBeEmpty();

            // Campaign should be marked as completed
            $completedCampaign = $campaign->markAsCompleted();
            $this->campaignRepo->save($completedCampaign);

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

    describe('Statistics and Monitoring', function () {
        it('provides accurate campaign statistics', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Stats Test Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:stats123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

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

            expect($stats)->toBeArray();
            expect($stats)->toHaveKey('campaign');
            expect($stats)->toHaveKey('total_registrations');
            expect($stats)->toHaveKey('days_until_release');
            expect($stats['campaign'])->toBe($campaign);
            expect($stats['days_until_release'])->toBe(7);
        });

        it('tracks registration status distribution', function () {
            $campaign = PreSaveCampaign::create(
                title: 'Distribution Test Album',
                artistName: 'Test Artist',
                coverImageUrl: 'https://example.com/cover.jpg',
                releaseDate: Timestamp::fromDateTime(new \DateTimeImmutable('+7 days')),
                trackUrls: [TrackUrl::create(StreamingPlatform::SPOTIFY, 'spotify:track:dist123')]
            );
            $campaign = $this->campaignRepo->save($campaign->publish());

            // Create registrations with different statuses
            $pending = PreSaveRegistration::create($campaign->id, 'user1', StreamingPlatform::SPOTIFY);
            $this->registrationRepo->save($pending);

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

            $failed = PreSaveRegistration::create($campaign->id, 'user3', StreamingPlatform::SPOTIFY);
            $this->registrationRepo->save($failed->markAsFailed('Test error'));

            $counts = $this->registrationRepo->getStatusCounts($campaign->id);

            expect($counts['pending'])->toBe(1);
            expect($counts['completed'])->toBe(1);
            expect($counts['failed'])->toBe(1);
        });
    });
});
