<?php

use App\Livewire\Admin\CustomerDetail;
use App\Livewire\Admin\MailLog;
use App\Livewire\Admin\Overview;
use App\Mail\OperatorMessageMail;
use App\Models\Customer;
use App\Models\SentMail;
use App\Models\Subscription;
use App\Models\SupportRequest;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;

/**
 * What left the building, and who wrote it.
 *
 * "Hat der Kunde die Zugangsdaten je bekommen, und wann?" was answered on the
 * mail server until now — a different machine, a different program, and nothing
 * an operator can put in front of somebody who says nothing arrived.
 */
it('writes a row for every mail the application sends', function () {
    $customer = Customer::factory()->create();

    Mail::to($customer->email)->send(new OperatorMessageMail($customer, 'Betreff', 'Text'));

    $row = SentMail::query()->latest('id')->first();

    expect($row)->not->toBeNull()
        ->and($row->to)->toBe($customer->email)
        ->and($row->subject)->toBe('Betreff')
        ->and($row->mailable)->toBe(OperatorMessageMail::class)
        // Attached to the customer, so the question "what did THIS customer
        // get" does not mean reading the whole register.
        ->and($row->customer_id)->toBe($customer->id);
});

/**
 * Eine Mail, eine Zeile.
 *
 * RecordSentMail liegt in app/Listeners, und Laravel meldet die Zuhörer dort
 * von selbst an. Eine zweite Anmeldung von Hand im AppServiceProvider wirkte
 * deshalb nicht wie eine Bekräftigung, sondern wie ein zweiter Zuhörer: jede
 * Mail hinterließ zwei Zeilen. An einer einzelnen Mail fällt das niemandem auf
 * — es fiel bei einem Trockenlauf auf, der fünfzig Mails verschickte und
 * hundert Zeilen vorfand.
 */
it('writes exactly one row per recipient, however the listener is registered', function () {
    $customer = Customer::factory()->create();

    Mail::to($customer->email)->send(new OperatorMessageMail($customer, 'Betreff', 'Text'));

    expect(SentMail::query()->where('to', $customer->email)->count())->toBe(1);
});

/**
 * Die Wanderung, die den Altbestand geradezieht.
 *
 * Der doppelte Zuhörer hat jeden Versand exakt verdoppelt, also bleibt von
 * jeder Gruppe gleicher Zeilen die ältere Hälfte stehen — aus vier werden zwei
 * (das waren zwei echte Versande), aus zwei wird eine, und was einzeln steht,
 * bleibt unberührt.
 */
it('halves the duplicated rows the doubled listener left behind', function () {
    $zeile = fn (string $to, string $subject, string $zeit) => SentMail::create([
        'to' => $to, 'subject' => $subject, 'mailable' => \App\Mail\OperatorMessageMail::class,
        'from_operator' => false, 'sent_at' => $zeit,
    ]);

    // Zwei echte Versande, verdoppelt.
    foreach (range(1, 4) as $i) {
        $zeile('vier@example.com', 'Vier', '2026-07-30 12:00:00');
    }
    // Ein echter Versand, verdoppelt.
    foreach (range(1, 2) as $i) {
        $zeile('zwei@example.com', 'Zwei', '2026-07-30 12:00:01');
    }
    // Nach der Behebung geschrieben — steht schon richtig da.
    $zeile('eins@example.com', 'Eins', '2026-07-30 12:00:02');

    (require database_path('migrations/2026_08_04_100000_das_register_zaehlt_jeden_versand_einmal.php'))->up();

    expect(SentMail::query()->where('to', 'vier@example.com')->count())->toBe(2)
        ->and(SentMail::query()->where('to', 'zwei@example.com')->count())->toBe(1)
        ->and(SentMail::query()->where('to', 'eins@example.com')->count())->toBe(1);
});

it('records the send, not the intention', function () {
    // On MessageSent, not MessageSending: a row here has to mean the transport
    // accepted it. Recording intentions would fill the register with mails that
    // never left — worse than no register, because an operator would show a
    // customer a line proving something was sent that was not.
    expect(config('mail.default'))->not->toBeNull();

    $before = SentMail::query()->count();
    Mail::fake();

    $customer = Customer::factory()->create();
    Mail::to($customer->email)->send(new OperatorMessageMail($customer, 'Betreff', 'Text'));

    // Mail::fake() never hands the message to a transport, so nothing was sent
    // and nothing is claimed.
    expect(SentMail::query()->count())->toBe($before);
});

it('keeps a mail to an address nobody owns, without inventing a customer for it', function () {
    Mail::to('fremder@example.test')->send(
        new OperatorMessageMail(Customer::factory()->create(), 'Betreff', 'Text')
    );

    $row = SentMail::query()->where('to', 'fremder@example.test')->first();

    expect($row)->not->toBeNull()
        ->and($row->customer_id)->toBeNull();
});

it('does not let a stranger write rows by asking for a password reset', function () {
    // The verification and reset mails go to addresses nobody has confirmed,
    // and anyone who can type into a form can send them. Recording them would
    // hand the operator's register to whoever wants to fill it.
    $user = App\Models\User::factory()->create();

    Mail::to($user->email)->send(new App\Mail\ResetPasswordMail($user, 'https://example.test/reset', 60));

    expect(SentMail::query()->where('to', $user->email)->exists())->toBeFalse();
});

it('shows the register, newest first, with the customer it belongs to', function () {
    $customer = Customer::factory()->create(['name' => 'Kanzlei Berger']);

    SentMail::create([
        'customer_id' => $customer->id,
        'to' => $customer->email,
        'subject' => 'Ihre Zugangsdaten',
        'mailable' => App\Mail\OperatorMessageMail::class,
        'sent_at' => now()->subHour(),
    ]);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(MailLog::class)
        ->assertOk()
        ->assertSee('Ihre Zugangsdaten')
        ->assertSee('Kanzlei Berger');
});

// ---- Writing to a customer from the console ----

it('sends the operator message and records what was written', function () {
    // The body is kept only for a mail a person typed: "what exactly did I
    // write to them in March" is a question the mail server's log cannot
    // answer either.
    $customer = Customer::factory()->create();

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(CustomerDetail::class, ['uuid' => $customer->uuid])
        ->set('subject', 'Zur Datenübernahme')
        ->set('body', 'Wir haben uns Ihr System angesehen und können übernehmen.')
        ->call('send');

    $row = SentMail::query()->where('customer_id', $customer->id)->latest('id')->first();

    expect($row)->not->toBeNull()
        ->and($row->subject)->toBe('Zur Datenübernahme')
        ->and($row->from_operator)->toBeTrue()
        ->and($row->body)->toBe('Wir haben uns Ihr System angesehen und können übernehmen.');
});

it('closes the request it answers, which is the reason to answer from here', function () {
    // Answering from a mail client leaves the request open for ever, because
    // nothing ever told it otherwise.
    $customer = Customer::factory()->create();
    $request = SupportRequest::create([
        'customer_id' => $customer->id,
        'subject' => 'Können Sie unsere Daten übernehmen?',
        'category' => 'other',
        'body' => 'Wir haben ownCloud 10 im Haus.',
        'status' => 'open',
        'reported_by' => $customer->email,
    ]);

    $page = Livewire::actingAs(operator('Owner'), 'operator')
        ->test(CustomerDetail::class, ['uuid' => $customer->uuid])
        ->call('answer', $request->uuid);

    expect($page->get('subject'))->toContain('Können Sie unsere Daten übernehmen?');

    $page->set('body', 'Ja, das geht — Angebot folgt.')->call('send');

    expect($request->fresh()->status)->toBe('answered')
        ->and($request->fresh()->answered_at)->not->toBeNull();
});

it('shows the customer their own words, so nobody opens the mail client to read the question', function () {
    $customer = Customer::factory()->create();
    SupportRequest::create([
        'customer_id' => $customer->id,
        'subject' => 'Frage zur Migration',
        'category' => 'other',
        'body' => 'Wir haben 42 GB in ownCloud und wollen wechseln.',
        'status' => 'open',
        'reported_by' => $customer->email,
    ]);

    // On the messages tab since the page was rebuilt around tabs: the details
    // are the first thing an operator needs, the conversation the second.
    Livewire::withQueryParams(['tab' => 'messages'])
        ->actingAs(operator('Owner'), 'operator')
        ->test(CustomerDetail::class, ['uuid' => $customer->uuid])
        ->assertSee('Wir haben 42 GB in ownCloud und wollen wechseln.');
});

it('refuses an empty message rather than sending one', function () {
    $customer = Customer::factory()->create();

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(CustomerDetail::class, ['uuid' => $customer->uuid])
        ->call('send')
        ->assertHasErrors(['subject', 'body']);

    expect(SentMail::query()->where('customer_id', $customer->id)->exists())->toBeFalse();
});

it('keeps both pages to operators who may see customers', function () {
    // Support KEEPS both, deliberately: answering customers is the job, and a
    // support operator who cannot read what was already sent them is answering
    // blind. Read-only is the role that may look at the console and change
    // nothing — writing a mail is a change, and so is reading the register of
    // what was written.
    $customer = Customer::factory()->create();

    expect(operator('Support')->can('customers.manage'))->toBeTrue();

    Livewire::actingAs(operator('Read-only'), 'operator')
        ->test(CustomerDetail::class, ['uuid' => $customer->uuid])
        ->assertForbidden();

    Livewire::actingAs(operator('Read-only'), 'operator')
        ->test(MailLog::class)
        ->assertForbidden();
});

// ---- The notices somebody can finally act on ----

it('gives every notice the page that shows the thing it is about', function () {
    // The count in the header said "4 Hinweis(e)" and led nowhere: whoever read
    // it had to know the list was a card further down the same page, and then
    // go looking for the host or the run by hand.
    App\Models\Host::factory()->create(['status' => 'error', 'name' => 'pve-broken']);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(Overview::class)
        ->assertViewHas('notices', fn (array $notices) => collect($notices)
            ->every(fn (array $n) => array_key_exists('route', $n) && $n['route'] !== null))
        ->assertSee(route('admin.hosts'), false);
});

it('counts notices in words, not in brackets', function () {
    expect(trans_choice('admin.systems_notices', 1, ['n' => 1]))->toBe('1 Hinweis')
        ->and(trans_choice('admin.systems_notices', 4, ['n' => 4]))->toBe('4 Hinweise');
});

it('shows one customer their own things and never another customer', function () {
    // Reported as "unterhalb steht immer das selbe, egal bei welchem Kunden".
    // Worth proving rather than assuming: every list on this page is scoped to
    // the customer in the URL, and two customers side by side must not bleed
    // into each other.
    $one = Customer::factory()->create(['name' => 'Erste Kanzlei']);
    $two = Customer::factory()->create(['name' => 'Zweite Kanzlei']);

    SupportRequest::create([
        'customer_id' => $one->id, 'subject' => 'Frage der Ersten', 'category' => 'other',
        'body' => 'Text der Ersten', 'status' => 'open', 'reported_by' => $one->email,
    ]);
    SupportRequest::create([
        'customer_id' => $two->id, 'subject' => 'Frage der Zweiten', 'category' => 'other',
        'body' => 'Text der Zweiten', 'status' => 'open', 'reported_by' => $two->email,
    ]);

    SentMail::create(['customer_id' => $one->id, 'to' => $one->email, 'subject' => 'Post an die Erste', 'sent_at' => now()]);
    SentMail::create(['customer_id' => $two->id, 'to' => $two->email, 'subject' => 'Post an die Zweite', 'sent_at' => now()]);

    Livewire::withQueryParams(['tab' => 'messages'])
        ->actingAs(operator('Owner'), 'operator')
        ->test(CustomerDetail::class, ['uuid' => $one->uuid])
        ->assertSee('Frage der Ersten')
        ->assertSee('Post an die Erste')
        ->assertDontSee('Frage der Zweiten')
        ->assertDontSee('Post an die Zweite');
});

it('names the status in words rather than printing the key at it', function () {
    // The badge read "customers.status.active" — a lang key with no file behind
    // it renders as itself, and Laravel says nothing about it.
    $customer = Customer::factory()->create(['status' => 'active']);
    SupportRequest::create([
        'customer_id' => $customer->id, 'subject' => 'Frage', 'category' => 'other',
        'body' => 'Text', 'status' => 'open', 'reported_by' => $customer->email,
    ]);

    // The status badge is in the page header, on every tab; the request status
    // lives with the requests.
    Livewire::withQueryParams(['tab' => 'messages'])
        ->actingAs(operator('Owner'), 'operator')
        ->test(CustomerDetail::class, ['uuid' => $customer->uuid])
        ->assertSee(__('admin.status.active'))
        ->assertSee(__('support.status_open'))
        // Neither key leaks through as its own name.
        ->assertDontSee('customers.status.')
        ->assertDontSee('support.status.');
});

// ---- Two things the console owed the operator ----

it('says on the front page that the site is switched off', function () {
    // Asked as "ich komme nicht auf www.dev". The site answers 503 to every
    // visitor who is not on the VPN, which is exactly what a broken deployment
    // looks like from outside — and the only place that said so was the switch
    // itself, on another page.
    App\Support\Settings::set('site.public', false);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(Overview::class)
        ->assertSee(__('admin.notice.site_hidden'))
        // And it leads to the switch.
        ->assertSee(route('admin.settings'), false);

    App\Support\Settings::set('site.public', true);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(Overview::class)
        ->assertDontSee(__('admin.notice.site_hidden'));
});

it('says when the website has no hostname of its own', function () {
    // Without SITE_HOST the marketing site is registered without a hostname, so
    // it answers on the PORTAL's host too — and every route() it generates
    // points at APP_URL. That is why a link on the site lands on app.…, which
    // was the other half of the same report.
    // Only fires where the installation is host-separated at all: on a fresh
    // checkout nothing is bound, and a warning there would be furniture.
    config()->set('admin_access.hosts', ['admin.dev.example.test']);
    config()->set('admin_access.site_hosts', []);
    config()->set('app.url', 'https://app.dev.example.test');

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(Overview::class)
        ->assertSee('app.dev.example.test');

    config()->set('admin_access.site_hosts', ['www.dev.example.test']);

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(Overview::class)
        ->assertDontSee(__('admin.notice.no_site_host', ['app' => 'app.dev.example.test']));
});

it('says when no mail is being delivered at all', function () {
    // The report: "ich versuche mich zu registrieren, erhalte aber keine Verify-
    // Mail, auch nicht beim erneuten Senden". MAIL_MAILER=log short-circuits
    // every purpose mailer to the log transport (MailboxTransport::
    // NON_DELIVERING) however well the mailboxes are configured — and the
    // failure is silent in every direction: the queue reports success, no job
    // fails, and the console's own mailbox test reports success too because it
    // builds its own transport on purpose. Nothing on any page said so.
    config()->set('mail.default', 'log');

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(Overview::class)
        ->assertSee(__('admin.notice.mail_not_delivering', ['mailer' => 'log']))
        ->assertSee(route('admin.mail'), false);

    // And on the page somebody actually opens to find out why.
    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(App\Livewire\Admin\Mail::class)
        ->assertSee(__('admin.notice.mail_not_delivering', ['mailer' => 'log']));
});

it('stays quiet once mail really goes out', function () {
    config()->set('mail.default', 'smtp');

    Livewire::actingAs(operator('Owner'), 'operator')
        ->test(Overview::class)
        ->assertDontSee(__('admin.notice.mail_not_delivering', ['mailer' => 'log']));
});

it('names every mailer that swallows mail, not only the log one', function () {
    // array and an unset default do the same thing, and the transport already
    // treats all three alike. A notice that only knew about 'log' would be
    // silent in the two cases nobody thinks to check.
    $swallowing = (new ReflectionClass(App\Mail\Transport\MailboxTransport::class))
        ->getConstant('NON_DELIVERING');

    expect($swallowing)->toBe(['log', 'array', null]);

    foreach (['array', null] as $mailer) {
        config()->set('mail.default', $mailer);

        Livewire::actingAs(operator('Owner'), 'operator')
            ->test(Overview::class)
            ->assertSee(__('admin.notice.mail_not_delivering', ['mailer' => $mailer ?? 'null']));
    }
});

it('does not invent a package for a customer who has booked nothing', function () {
    // Reported straight after registering: the dashboard said "noch keine
    // Instanz", and the package page said "AKTUELLES PAKET Start — Status Aktiv"
    // about the same account. currentKey fell back to 'start', and the view
    // rendered that fallback as a contract the customer had entered into.
    $user = App\Models\User::factory()->create(['email_verified_at' => now()]);
    Customer::factory()->create(['email' => $user->email, 'user_id' => $user->id]);

    Livewire::actingAs($user)
        ->test(App\Livewire\Billing::class)
        ->assertViewHas('hasContract', false)
        ->assertSee(__('billing.none_title'))
        // And no invented status beside a plan name.
        ->assertDontSee(__('billing.current_plan'));
});

it('shows the contract of a customer whose order is still waiting for a machine', function () {
    // Resolved from the CUSTOMER, not from the instance: a paid order parked for
    // want of capacity has a subscription and no instance yet, and asking the
    // instance would report "no package" at somebody who has paid.
    $user = App\Models\User::factory()->create(['email_verified_at' => now()]);
    $customer = Customer::factory()->create(['email' => $user->email, 'user_id' => $user->id]);
    Subscription::factory()->plan('team')->create([
        'customer_id' => $customer->id,
        'status' => 'active',
        'instance_id' => null,
    ]);

    Livewire::actingAs($user)
        ->test(App\Livewire\Billing::class)
        ->assertViewHas('hasContract', true)
        ->assertSee(__('billing.plan.team'))
        ->assertDontSee(__('billing.none_title'));
});
