<?php
declare(strict_types=1);

namespace FreshCloud\Mail\Tests\Compose;

use PHPUnit\Framework\TestCase;
use FreshCloud\Mail\Compose\Composer;
use FreshCloud\Mail\PocketBaseAdapter\PocketBaseAdapter;
use FreshCloud\Mail\PocketBaseAdapter\CollectionAdapter;
use FreshCloud\Mail\Compose\ComposeRequest;
use FreshCloud\Mail\Compose\ComposeResult;
use FreshCloud\Mail\Compose\Draft;
use FreshCloud\Mail\Log\Logger;
use FreshCloud\Mail\Smtp\SmtpClient;
use FreshCloud\Mail\MailClient\MailClient;
use FreshCloud\Mail\Identity\IdentityProvider;

/**
 * Tests for the Composer class.
 * Per formation §11 — 6 user-simulation scenarios.
 * Uses in-memory test doubles (no real PB/SMTP).
 */
final class ComposerTest extends TestCase
{
    private InMemoryPb $pb;
    private CapturingLogger $logger;
    private FakeSmtp $smtp;
    private FakeMailClient $mailClient;
    private FakeIdentity $identity;
    private Composer $composer;

    protected function setUp(): void
    {
        $this->pb = new InMemoryPb();
        $this->logger = new CapturingLogger();
        $this->smtp = new FakeSmtp();
        $this->mailClient = new FakeMailClient();
        $this->identity = new FakeIdentity();

        $this->composer = new Composer(
            $this->pb,
            $this->logger,
            $this->smtp,
            $this->mailClient,
            $this->identity
        );
    }

    /**
     * @test
     *
     * Scenario 1: compose new, send, verify sent.
     */
    public function new_then_send_succeeds(): void
    {
        // Given
        $this->identity->setFromAddress('user-1', 'acct-1', 'me@example.com');
        $this->smtp->setNextMessageId('<abc123@example.com>');

        $request = ComposeRequest::fromArray([
            'mode' => 'new',
            'to' => ['alice@example.com'],
            'subject' => 'Hello',
            'body_html' => '<p>Hi there</p>',
            'body_text' => 'Hi there',
            'account_id' => 'acct-1',
            'save_as_draft' => false,
        ]);

        // When
        $result = $this->composer->send('user-1', 'acct-1', $request);

        // Then
        $this->assertSame(\FreshCloud\Mail\Compose\ComposeStatus::Sent, $result->status);
        $this->assertNotEmpty($result->messageId);
        $this->assertTrue($this->smtp->wasCalled);
        $this->assertSame('me@example.com', $this->smtp->lastFrom);
        $this->assertContains('alice@example.com', $this->smtp->lastTo);
        $this->assertSame('Hello', $this->smtp->lastSubject);
    }

    /**
     * @test
     *
     * Scenario 2: compose new, save draft, resume, verify data preserved.
     */
    public function new_then_save_then_resume_preserves_data(): void
    {
        // Given
        $this->identity->setFromAddress('user-1', 'acct-1', 'me@example.com');
        $draftId = $this->composer->start('user-1', 'acct-1', 'new');

        $request = ComposeRequest::fromArray([
            'mode' => 'new',
            'to' => ['bob@example.com'],
            'subject' => 'Draft subject',
            'body_text' => 'Draft body',
            'account_id' => 'acct-1',
            'save_as_draft' => true,
        ]);

        // When
        $this->composer->saveDraft($draftId, $request, 'user-1');
        $resumed = $this->composer->resumeDraft($draftId, 'user-1');

        // Then
        $this->assertSame(['bob@example.com'], $resumed->to);
        $this->assertSame('Draft subject', $resumed->subject);
        $this->assertSame('Draft body', $resumed->bodyText);
    }

    /**
     * @test
     *
     * Scenario 3: reply to existing message, verify original is quoted.
     */
    public function reply_quotes_original_message(): void
    {
        // Given
        $this->mailClient->setMessage('user-1', 'acct-1', 'msg-1', [
            'from' => 'alice@example.com',
            'to' => ['me@example.com'],
            'cc' => [],
            'subject' => 'Original',
            'bodyHtml' => '<p>Hello there</p>',
            'bodyText' => 'Hello there',
            'emlPath' => null,
        ]);

        // When
        $draftId = $this->composer->start('user-1', 'acct-1', 'reply', 'msg-1');

        // Then
        $draft = $this->pb->get('email_drafts', $draftId);
        $this->assertSame(['alice@example.com'], $draft['to_json'] ? json_decode($draft['to_json'], true) : []);
        $this->assertSame('Re: Original', $draft['subject']);
        $this->assertStringContainsString('Hello there', $draft['body_html']);
    }

    /**
     * @test
     *
     * Scenario 4: forward, original message attached as quoted.
     */
    public function forward_attaches_original_message(): void
    {
        // Given
        $this->mailClient->setMessage('user-1', 'acct-1', 'msg-1', [
            'from' => 'alice@example.com',
            'to' => ['me@example.com'],
            'cc' => [],
            'subject' => 'Original',
            'bodyHtml' => '<p>Forward this</p>',
            'bodyText' => 'Forward this',
            'emlPath' => '/tmp/msg-1.eml',
        ]);

        // When
        $draftId = $this->composer->start('user-1', 'acct-1', 'forward', null, 'msg-1');

        // Then
        $draft = $this->pb->get('email_drafts', $draftId);
        $this->assertSame('Fwd: Original', $draft['subject']);
        $this->assertStringContainsString('Forward this', $draft['body_html']);
    }

    /**
     * @test
     *
     * Scenario 5: send with invalid email returns Failed.
     */
    public function send_with_invalid_email_fails(): void
    {
        // Given
        $this->identity->setFromAddress('user-1', 'acct-1', 'me@example.com');
        $request = ComposeRequest::fromArray([
            'mode' => 'new',
            'to' => ['not-an-email'],
            'subject' => 'Hello',
            'body_text' => 'Hi',
            'account_id' => 'acct-1',
            'save_as_draft' => false,
        ]);

        // When
        $result = $this->composer->send('user-1', 'acct-1', $request);

        // Then
        $this->assertSame(\FreshCloud\Mail\Compose\ComposeStatus::Failed, $result->status);
        $this->assertStringContainsString('not-an-email', $result->error ?? '');
        $this->assertFalse($this->smtp->wasCalled);
    }

    /**
     * @test
     *
     * Scenario 6: delete draft removes it from PB.
     */
    public function delete_draft_removes_from_pb(): void
    {
        // Given
        $draftId = $this->composer->start('user-1', 'acct-1', 'new');
        $this->assertNotNull($this->pb->get('email_drafts', $draftId));

        // When
        $this->composer->deleteDraft($draftId, 'user-1');

        // Then
        $this->expectException(\RuntimeException::class);
        $this->pb->get('email_drafts', $draftId);
    }
}

/**
 * In-memory PocketBase stand-in.
 * Implements the subset of $pb->collection()->create/get/update/delete used by Composer.
 */
final class InMemoryPb implements PocketBaseAdapter
{
    /** @var array<string, array<string, array<string, mixed>>> */
    public array $store = [];

    public function collection(string $name): CollectionAdapter
    {
        return new class($this, $name) implements \FreshCloud\Mail\PocketBaseAdapter\CollectionAdapter {
            public function __construct(private InMemoryPb $pb, private string $name) {}

            public function create(array $data): array
            {
                $id = $data['id'] ?? bin2hex(random_bytes(8));
                $data['id'] = $id;
                $this->pb->store[$this->name][$id] = $data;
                return $data;
            }

            public function getOne(string $id): array
            {
                if (!isset($this->pb->store[$this->name][$id])) {
                    throw new \RuntimeException("Not found: $id");
                }
                return $this->pb->store[$this->name][$id];
            }

            public function getList(int $page, int $perPage, array $opts = []): array
            {
                $items = array_values($this->pb->store[$this->name] ?? []);
                if (isset($opts['filter'])) {
                    // very basic filter parser: id="..."
                    if (preg_match('/id="([^"]+)"/', $opts['filter'], $m)) {
                        $items = array_filter($items, fn($i) => ($i['id'] ?? null) === $m[1]);
                    }
                }
                return ['items' => array_values($items)];
            }

            public function update(string $id, array $data): array
            {
                if (!isset($this->pb->store[$this->name][$id])) {
                    throw new \RuntimeException("Not found: $id");
                }
                $this->pb->store[$this->name][$id] = array_merge($this->pb->store[$this->name][$id], $data);
                return $this->pb->store[$this->name][$id];
            }

            public function delete(string $id): void
            {
                if (!isset($this->pb->store[$this->name][$id])) {
                    throw new \RuntimeException("Not found: $id");
                }
                unset($this->pb->store[$this->name][$id]);
            }
        };
    }

    public function get(string $collection, string $id): array
    {
        if (!isset($this->store[$collection][$id])) {
            throw new \RuntimeException("Not found: $collection/$id");
        }
        return $this->store[$collection][$id];
    }
}

/**
 * Logger that records all log calls.
 */
final class CapturingLogger implements Logger
{
    /** @var array<array{event: string, context: array}> */
    public array $calls = [];

    public function write(\FreshCloud\Mail\Log\LogType $level, string $message, array $context = []): void
    {
        $this->calls[] = ['event' => $message, 'context' => $context, 'level' => $level->name];
    }

    public function audit(string $action, array $context = []): void
    {
        $this->calls[] = ['event' => 'audit:' . $action, 'context' => $context, 'level' => 'Info'];
    }

    public function flush(): void
    {
        // no-op for tests
    }

    public function getRecentActivity(string $userId, int $limit = 50): array
    {
        return [];
    }

    public function getRecentErrors(string $userId, \DateTimeInterface $since): array
    {
        return [];
    }

    public function addDriver(\FreshCloud\Mail\Log\Driver $driver): void
    {
        // no-op for tests
    }

    public function log(string $event, array $context = []): void
    {
        $this->calls[] = ['event' => $event, 'context' => $context, 'level' => 'Info'];
    }
}

/**
 * SMTP client that records the last call and returns a fake message_id.
 */
final class FakeSmtp implements SmtpClient
{
    public bool $wasCalled = false;
    public ?string $lastFrom = null;
    public array $lastTo = [];
    public ?string $lastSubject = null;
    public ?string $lastBodyHtml = null;
    public ?string $lastBodyText = null;
    public ?string $nextMessageId = '<fake@example.com>';

    public function setNextMessageId(string $id): void
    {
        $this->nextMessageId = $id;
    }

    public function send(
        string $from,
        array $to,
        string $subject,
        string $bodyHtml,
        string $bodyText,
        array $attachmentPaths
    ): string {
        $this->wasCalled = true;
        $this->lastFrom = $from;
        $this->lastTo = $to;
        $this->lastSubject = $subject;
        $this->lastBodyHtml = $bodyHtml;
        $this->lastBodyText = $bodyText;
        return $this->nextMessageId ?? '<fake@example.com>';
    }
}

/**
 * Mail client that returns pre-set messages.
 */
final class FakeMailClient implements MailClient
{
    /** @var array<string, array> */
    private array $messages = [];

    public function setMessage(string $userId, string $accountId, string $msgId, array $message): void
    {
        $this->messages["$userId/$accountId/$msgId"] = $message;
    }

    public function getMessage(string $userId, string $accountId, string $msgId): array
    {
        $key = "$userId/$accountId/$msgId";
        if (!isset($this->messages[$key])) {
            throw new \RuntimeException("Message not found: $key");
        }
        return $this->messages[$key];
    }
}

/**
 * Identity provider that returns pre-set From addresses.
 */
final class FakeIdentity implements IdentityProvider
{
    /** @var array<string, string> */
    private array $addresses = [];

    public function setFromAddress(string $userId, string $accountId, string $email): void
    {
        $this->addresses["$userId/$accountId"] = $email;
    }

    public function getFromAddress(string $userId, string $accountId): string
    {
        $key = "$userId/$accountId";
        return $this->addresses[$key] ?? '';
    }

    public function getAllAccounts(string $userId): array
    {
        $out = [];
        foreach ($this->addresses as $key => $email) {
            [$u, $aid] = explode('/', $key, 2);
            if ($u === $userId) {
                $out[] = ['id' => $aid, 'email' => $email, 'name' => $email];
            }
        }
        return $out;
    }

    public function getActiveAccountId(string $userId): string
    {
        foreach ($this->addresses as $key => $email) {
            [$u, $aid] = explode('/', $key, 2);
            if ($u === $userId) return $aid;
        }
        return '';
    }

    public function setActiveAccountId(string $userId, string $accountId): bool
    {
        return true;
    }
}
