<?php

declare(strict_types=1);

use App\Framework\DateTime\SystemClock;
use App\Framework\MagicLinks\MagicLinkToken;
use App\Framework\MagicLinks\Services\InMemoryMagicLinkService;
use App\Framework\MagicLinks\TokenAction;
use App\Framework\MagicLinks\TokenConfig;
use App\Framework\Ulid\UlidGenerator;

beforeEach(function () {
    $this->clock = new SystemClock();
    $this->ulidGenerator = new UlidGenerator();
    $this->service = new InMemoryMagicLinkService($this->ulidGenerator, $this->clock);
});

describe('InMemoryMagicLinkService - Token Generation', function () {
    it('generates valid token', function () {
        $action = new TokenAction('email_verification');
        $payload = ['user_id' => 123, 'email' => 'test@example.com'];

        $token = $this->service->generate($action, $payload);

        expect($token)->toBeInstanceOf(MagicLinkToken::class);
        expect(strlen($token->value))->toBeGreaterThanOrEqual(16);
    });

    it('generates unique tokens for each call', function () {
        $action = new TokenAction('test_action');
        $payload = ['data' => 'test'];

        $token1 = $this->service->generate($action, $payload);
        $token2 = $this->service->generate($action, $payload);

        expect($token1->equals($token2))->toBeFalse();
    });

    it('stores token with default configuration', function () {
        $action = new TokenAction('test_action');
        $payload = ['test' => 'data'];

        $token = $this->service->generate($action, $payload);

        expect($this->service->exists($token))->toBeTrue();
    });

    it('stores token with custom configuration', function () {
        $action = new TokenAction('test_action');
        $payload = ['test' => 'data'];
        $config = new TokenConfig(expiryHours: 48, oneTimeUse: true);

        $token = $this->service->generate($action, $payload, $config);

        expect($this->service->exists($token))->toBeTrue();
    });

    it('stores IP address and user agent', function () {
        $action = new TokenAction('test_action');
        $payload = ['test' => 'data'];

        $token = $this->service->generate(
            $action,
            $payload,
            createdByIp: '192.168.1.1',
            userAgent: 'Test Browser'
        );

        $data = $this->service->validate($token);

        expect($data->createdByIp)->toBe('192.168.1.1');
        expect($data->userAgent)->toBe('Test Browser');
    });
});

describe('InMemoryMagicLinkService - Token Validation', function () {
    it('validates existing token', function () {
        $action = new TokenAction('test_action');
        $payload = ['user_id' => 123];

        $token = $this->service->generate($action, $payload);
        $data = $this->service->validate($token);

        expect($data)->toBeInstanceOf(\App\Framework\MagicLinks\MagicLinkData::class);
        expect($data->action->name)->toBe('test_action');
        expect($data->payload->get('user_id'))->toBe(123);
    });

    it('returns null for non-existent token', function () {
        $nonExistentToken = new MagicLinkToken('01234567890123456789012345678901');

        $data = $this->service->validate($nonExistentToken);

        expect($data)->toBeNull();
    });

    it('returns null for used one-time token', function () {
        $action = new TokenAction('test_action');
        $payload = ['test' => 'data'];
        $config = new TokenConfig(oneTimeUse: true);

        $token = $this->service->generate($action, $payload, $config);
        $this->service->markAsUsed($token);

        $data = $this->service->validate($token);

        expect($data)->toBeNull();
    });
});

describe('InMemoryMagicLinkService - Token Usage', function () {
    it('marks token as used', function () {
        $action = new TokenAction('test_action');
        $payload = ['test' => 'data'];
        $config = new TokenConfig(oneTimeUse: true);

        $token = $this->service->generate($action, $payload, $config);

        // Valid before use
        expect($this->service->validate($token))->toBeInstanceOf(\App\Framework\MagicLinks\MagicLinkData::class);

        $this->service->markAsUsed($token);

        // Invalid after use (one-time use)
        expect($this->service->validate($token))->toBeNull();
    });

    it('allows marking non-one-time tokens as used', function () {
        $action = new TokenAction('test_action');
        $payload = ['test' => 'data'];
        $config = new TokenConfig(oneTimeUse: false);

        $token = $this->service->generate($action, $payload, $config);
        $this->service->markAsUsed($token);

        // Still valid (not one-time use)
        expect($this->service->validate($token))->toBeInstanceOf(\App\Framework\MagicLinks\MagicLinkData::class);
    });

    it('handles marking non-existent token gracefully', function () {
        $nonExistentToken = new MagicLinkToken('01234567890123456789012345678901');

        // Should not throw exception
        $this->service->markAsUsed($nonExistentToken);

        expect(true)->toBeTrue();
    });
});

describe('InMemoryMagicLinkService - Token Revocation', function () {
    it('revokes token', function () {
        $action = new TokenAction('test_action');
        $payload = ['test' => 'data'];

        $token = $this->service->generate($action, $payload);

        // Exists before revocation
        expect($this->service->exists($token))->toBeTrue();

        $this->service->revoke($token);

        // Does not exist after revocation
        expect($this->service->exists($token))->toBeFalse();
        expect($this->service->validate($token))->toBeNull();
    });

    it('handles revoking non-existent token gracefully', function () {
        $nonExistentToken = new MagicLinkToken('01234567890123456789012345678901');

        // Should not throw exception
        $this->service->revoke($nonExistentToken);

        expect(true)->toBeTrue();
    });
});

describe('InMemoryMagicLinkService - Token Existence', function () {
    it('checks token existence correctly', function () {
        $action = new TokenAction('test_action');
        $payload = ['test' => 'data'];

        $token = $this->service->generate($action, $payload);

        expect($this->service->exists($token))->toBeTrue();
    });

    it('returns false for non-existent token', function () {
        $nonExistentToken = new MagicLinkToken('01234567890123456789012345678901');

        expect($this->service->exists($nonExistentToken))->toBeFalse();
    });

    it('returns false for revoked token', function () {
        $action = new TokenAction('test_action');
        $payload = ['test' => 'data'];

        $token = $this->service->generate($action, $payload);
        $this->service->revoke($token);

        expect($this->service->exists($token))->toBeFalse();
    });
});

describe('InMemoryMagicLinkService - Active Tokens', function () {
    it('retrieves active tokens', function () {
        $action = new TokenAction('test_action');

        // Generate multiple tokens
        $this->service->generate($action, ['id' => 1]);
        $this->service->generate($action, ['id' => 2]);
        $this->service->generate($action, ['id' => 3]);

        $activeTokens = $this->service->getActiveTokens();

        expect($activeTokens)->toHaveCount(3);
    });

    it('respects limit parameter', function () {
        $action = new TokenAction('test_action');

        // Generate 5 tokens
        for ($i = 1; $i <= 5; $i++) {
            $this->service->generate($action, ['id' => $i]);
        }

        $activeTokens = $this->service->getActiveTokens(limit: 3);

        expect($activeTokens)->toHaveCount(3);
    });

    it('excludes used one-time tokens from active list', function () {
        $action = new TokenAction('test_action');
        $config = new TokenConfig(oneTimeUse: true);

        $token1 = $this->service->generate($action, ['id' => 1], $config);
        $token2 = $this->service->generate($action, ['id' => 2], $config);

        $this->service->markAsUsed($token1);

        $activeTokens = $this->service->getActiveTokens();

        expect($activeTokens)->toHaveCount(1);
    });

    it('returns empty array when no active tokens', function () {
        $activeTokens = $this->service->getActiveTokens();

        expect($activeTokens)->toBeArray();
        expect($activeTokens)->toBeEmpty();
    });
});

describe('InMemoryMagicLinkService - Cleanup', function () {
    it('removes expired tokens', function () {
        $action = new TokenAction('test_action');

        // Create token, then manually expire it by waiting
        // We'll test cleanup by generating tokens and checking count
        // Since we can't create expired tokens directly (validation prevents it),
        // we'll just verify cleanup works with no expired tokens
        $config = new TokenConfig(expiryHours: 1);

        $token1 = $this->service->generate($action, ['id' => 1], $config);
        $token2 = $this->service->generate($action, ['id' => 2], $config);

        // Initially should have 2 tokens
        expect($this->service->getActiveTokens())->toHaveCount(2);

        // Cleanup with no expired tokens should return 0
        $removedCount = $this->service->cleanupExpired();
        expect($removedCount)->toBe(0);

        // Both tokens should still exist
        expect($this->service->exists($token1))->toBeTrue();
        expect($this->service->exists($token2))->toBeTrue();
    });

    it('returns zero when no expired tokens', function () {
        $action = new TokenAction('test_action');
        $config = new TokenConfig(expiryHours: 24);

        $this->service->generate($action, ['id' => 1], $config);
        $this->service->generate($action, ['id' => 2], $config);

        $removedCount = $this->service->cleanupExpired();

        expect($removedCount)->toBe(0);
    });

    it('handles empty token list', function () {
        $removedCount = $this->service->cleanupExpired();

        expect($removedCount)->toBe(0);
    });
});
