<?php

declare(strict_types=1);

namespace Tests\Unit\Framework\Audit;

use App\Framework\Audit\InMemoryAuditLogger;
use App\Framework\Audit\ValueObjects\AuditableAction;
use App\Framework\Audit\ValueObjects\AuditEntry;
use App\Framework\Audit\ValueObjects\AuditQuery;
use App\Framework\DateTime\SystemClock;
use App\Framework\Http\IpAddress;

describe('AuditLogger', function () {
    beforeEach(function () {
        $this->logger = new InMemoryAuditLogger();
        $this->clock = new SystemClock();
    });

    it('logs audit entries', function () {
        $entry = AuditEntry::create(
            clock: $this->clock,
            action: AuditableAction::CREATE,
            entityType: 'User',
            entityId: '123',
            userId: 'admin',
            metadata: ['name' => 'John Doe']
        );

        $this->logger->log($entry);

        $found = $this->logger->find($entry->id);

        expect($found)->not->toBeNull();
        expect($found->action)->toBe(AuditableAction::CREATE);
        expect($found->entityType)->toBe('User');
        expect($found->entityId)->toBe('123');
        expect($found->userId)->toBe('admin');
        expect($found->metadata)->toBe(['name' => 'John Doe']);
    });

    it('logs failed entries', function () {
        $entry = AuditEntry::failed(
            clock: $this->clock,
            action: AuditableAction::DELETE,
            entityType: 'User',
            entityId: '123',
            errorMessage: 'Permission denied',
            userId: 'user'
        );

        $this->logger->log($entry);

        $found = $this->logger->find($entry->id);

        expect($found->success)->toBeFalse();
        expect($found->errorMessage)->toBe('Permission denied');
    });

    it('queries by action', function () {
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'Post'));

        $query = AuditQuery::forAction(AuditableAction::CREATE);
        $results = $this->logger->query($query);

        expect($results)->toHaveCount(2);
        expect($results[0]->action)->toBe(AuditableAction::CREATE);
        expect($results[1]->action)->toBe(AuditableAction::CREATE);
    });

    it('queries by entity type', function () {
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '1'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '2'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'Post', '1'));

        $query = AuditQuery::forEntity('User');
        $results = $this->logger->query($query);

        expect($results)->toHaveCount(2);
        expect($results[0]->entityType)->toBe('User');
    });

    it('queries by entity id', function () {
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '123'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User', '123'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '456'));

        $query = AuditQuery::forEntity('User', '123');
        $results = $this->logger->query($query);

        expect($results)->toHaveCount(2);
        expect($results[0]->entityId)->toBe('123');
    });

    it('queries by user id', function () {
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', userId: 'admin'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User', userId: 'admin'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', userId: 'user'));

        $query = AuditQuery::forUser('admin');
        $results = $this->logger->query($query);

        expect($results)->toHaveCount(2);
        expect($results[0]->userId)->toBe('admin');
    });

    it('queries failed entries only', function () {
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User'));
        $this->logger->log(AuditEntry::failed($this->clock, AuditableAction::DELETE, 'User', errorMessage: 'Error'));
        $this->logger->log(AuditEntry::failed($this->clock, AuditableAction::UPDATE, 'User', errorMessage: 'Error'));

        $query = AuditQuery::failedOnly();
        $results = $this->logger->query($query);

        expect($results)->toHaveCount(2);
        expect($results[0]->success)->toBeFalse();
        expect($results[1]->success)->toBeFalse();
    });

    it('queries successful entries only', function () {
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User'));
        $this->logger->log(AuditEntry::failed($this->clock, AuditableAction::DELETE, 'User', errorMessage: 'Error'));

        $query = AuditQuery::successfulOnly();
        $results = $this->logger->query($query);

        expect($results)->toHaveCount(2);
        expect($results[0]->success)->toBeTrue();
        expect($results[1]->success)->toBeTrue();
    });

    it('queries by date range', function () {
        $now = new \DateTimeImmutable();
        $yesterday = $now->modify('-1 day');
        $tomorrow = $now->modify('+1 day');

        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User'));

        $query = AuditQuery::inDateRange($yesterday, $tomorrow);
        $results = $this->logger->query($query);

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

    it('combines multiple filters', function () {
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '1', 'admin'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User', '1', 'admin'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', '2', 'user'));

        $query = AuditQuery::forEntity('User', '1')
            ->withAction(AuditableAction::CREATE)
            ->withUserId('admin');

        $results = $this->logger->query($query);

        expect($results)->toHaveCount(1);
        expect($results[0]->action)->toBe(AuditableAction::CREATE);
        expect($results[0]->entityId)->toBe('1');
        expect($results[0]->userId)->toBe('admin');
    });

    it('paginates results', function () {
        for ($i = 1; $i <= 10; $i++) {
            $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User', (string) $i));
        }

        $query = AuditQuery::all()->withLimit(5)->withOffset(0);
        $page1 = $this->logger->query($query);

        $query = AuditQuery::all()->withLimit(5)->withOffset(5);
        $page2 = $this->logger->query($query);

        expect($page1)->toHaveCount(5);
        expect($page2)->toHaveCount(5);
    });

    it('counts entries', function () {
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User'));

        $query = AuditQuery::forAction(AuditableAction::CREATE);
        $count = $this->logger->count($query);

        expect($count)->toBe(2);
    });

    it('purges old entries', function () {
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::CREATE, 'User'));
        $this->logger->log(AuditEntry::create($this->clock, AuditableAction::UPDATE, 'User'));

        $future = new \DateTimeImmutable('+1 day');
        $deleted = $this->logger->purgeOlderThan($future);

        expect($deleted)->toBe(2);
        expect($this->logger->count(AuditQuery::all()))->toBe(0);
    });

    it('stores ip address', function () {
        $entry = AuditEntry::create(
            clock: $this->clock,
            action: AuditableAction::LOGIN,
            entityType: 'Session',
            ipAddress: IpAddress::from('192.168.1.1')
        );

        $this->logger->log($entry);
        $found = $this->logger->find($entry->id);

        expect($found->ipAddress)->not->toBeNull();
        expect((string) $found->ipAddress)->toBe('192.168.1.1');
    });

    it('converts to array', function () {
        $entry = AuditEntry::create(
            clock: $this->clock,
            action: AuditableAction::CREATE,
            entityType: 'User',
            entityId: '123',
            userId: 'admin',
            metadata: ['test' => 'value']
        );

        $array = $entry->toArray();

        expect($array)->toHaveKey('id');
        expect($array)->toHaveKey('action');
        expect($array)->toHaveKey('entity_type');
        expect($array)->toHaveKey('entity_id');
        expect($array)->toHaveKey('timestamp');
        expect($array)->toHaveKey('user_id');
        expect($array)->toHaveKey('metadata');
        expect($array['action'])->toBe('crud.create');
        expect($array['entity_type'])->toBe('User');
        expect($array['metadata'])->toBe(['test' => 'value']);
    });
});
