<?php

use App\Livewire\ConfirmCancelPackage;
use App\Livewire\ConfirmCloseAccount;
use App\Livewire\Settings;
use App\Models\Customer;
use App\Models\Instance;
use App\Models\Order;
use App\Models\Subscription;
use App\Models\User;
use App\Services\Stripe\FakeStripeClient;
use App\Services\Stripe\StripeClient;
use Livewire\Livewire;

function settingsSetup(bool $withInstance = true): array
{
    $user = User::factory()->create(['email' => 's@set.test', 'is_admin' => false]);
    $customer = Customer::factory()->create(['email' => 's@set.test', 'user_id' => $user->id, 'name' => 'Acme', 'status' => 'active']);
    if ($withInstance) {
        $order = Order::factory()->create(['customer_id' => $customer->id, 'plan' => 'team']);
        Instance::factory()->create(['customer_id' => $customer->id, 'order_id' => $order->id, 'plan' => 'team', 'status' => 'active', 'subdomain' => 'acme']);
    }

    return compact('user', 'customer');
}

it('gates settings to authenticated users', function () {
    $this->get(route('settings'))->assertRedirect('/login');
});

it('blocks a suspended customer from the portal', function () {
    $user = User::factory()->create(['email' => 'sus@set.test', 'is_admin' => false]);
    Customer::factory()->create(['email' => 'sus@set.test', 'user_id' => $user->id, 'status' => 'suspended']);

    $this->actingAs($user)->get(route('dashboard'))->assertRedirect('/login');
    expect(auth()->check())->toBeFalse();
});

it('blocks a closed customer from the portal', function () {
    $user = User::factory()->create(['email' => 'cl@set.test', 'is_admin' => false]);
    Customer::factory()->create(['email' => 'cl@set.test', 'user_id' => $user->id, 'status' => 'closed', 'closed_at' => now()]);

    $this->actingAs($user)->get(route('dashboard'))->assertRedirect('/login');
});

it('rejects cancelling a package that is not active', function () {
    ['user' => $user, 'customer' => $customer] = settingsSetup();
    $instance = $customer->instances()->first();
    $instance->update(['status' => 'cancellation_scheduled']);

    Livewire::actingAs($user)->test(ConfirmCancelPackage::class)
        ->set('confirmName', 'acme')
        ->call('cancelPackage');

    // Still scheduled — not re-transitioned or resurrected.
    expect($instance->fresh()->status)->toBe('cancellation_scheduled');
});

it('saves the company profile', function () {
    ['user' => $user, 'customer' => $customer] = settingsSetup();

    Livewire::actingAs($user)->test(Settings::class)
        ->set('companyName', 'Acme GmbH')
        ->set('vatId', 'ATU12345678')
        ->set('billingStreet', 'Hauptstraße 1')
        ->set('billingPostcode', '1010')
        ->set('billingCity', 'Wien')
        ->set('billingCountry', 'Österreich')
        ->call('saveProfile')
        ->assertHasNoErrors();

    $customer->refresh();
    expect($customer->name)->toBe('Acme GmbH')
        ->and($customer->vat_id)->toBe('ATU12345678')
        ->and($customer->billing_city)->toBe('Wien')
        ->and($customer->billing_postcode)->toBe('1010')
        // And the composed block every existing reader uses: street, then
        // postcode and town on one line, then the country.
        ->and($customer->billing_address)->toBe("Hauptstraße 1\n1010 Wien\nÖsterreich");
});

it('never lets a business turn itself into a consumer', function () {
    // The fourteen-day right of withdrawal hangs off this field. A business that
    // could set itself to "Privatperson" here is a business that can withdraw
    // from a contract it may not withdraw from — which is why registration asks
    // the question and why this form cannot answer it a second time.
    ['user' => $user, 'customer' => $customer] = settingsSetup();
    $customer->update(['customer_type' => App\Models\Customer::TYPE_BUSINESS]);

    Livewire::actingAs($user)->test(Settings::class)
        ->set('customerType', App\Models\Customer::TYPE_CONSUMER)
        ->call('saveProfile')
        ->assertHasNoErrors();

    expect($customer->fresh()->customer_type)->toBe(App\Models\Customer::TYPE_BUSINESS);
});

it('lets a record created before the question was asked answer it once', function () {
    ['user' => $user, 'customer' => $customer] = settingsSetup();
    $customer->update(['customer_type' => null]);

    Livewire::actingAs($user)->test(Settings::class)
        ->set('customerType', App\Models\Customer::TYPE_BUSINESS)
        ->call('saveProfile');

    expect($customer->fresh()->customer_type)->toBe(App\Models\Customer::TYPE_BUSINESS);

    // And then it is fixed like every other.
    Livewire::actingAs($user)->test(Settings::class)
        ->set('customerType', App\Models\Customer::TYPE_CONSUMER)
        ->call('saveProfile');

    expect($customer->fresh()->customer_type)->toBe(App\Models\Customer::TYPE_BUSINESS);
});

it('shows a recorded type as a fact, with no way to change it', function () {
    ['user' => $user, 'customer' => $customer] = settingsSetup();
    $customer->update(['customer_type' => App\Models\Customer::TYPE_BUSINESS]);

    $page = Livewire::actingAs($user)->test(Settings::class)->html();

    expect($page)->toContain(__('settings.customer_type_locked'))
        // No radio to press.
        ->and($page)->not->toContain('wire:model="customerType"');
});

it('saves branding and resolves defaults when unset', function () {
    ['user' => $user, 'customer' => $customer] = settingsSetup();

    // Unset → resolver reports defaults.
    expect($customer->brandingResolved()['is_default'])->toBeTrue();

    Livewire::actingAs($user)->test(Settings::class)
        ->set('brandDisplayName', 'Acme Cloud')
        ->set('brandPrimary', '#123456')
        ->call('saveBranding')
        ->assertHasNoErrors();

    $customer->refresh();
    expect($customer->brand_display_name)->toBe('Acme Cloud')
        ->and($customer->brandingResolved()['primary_color'])->toBe('#123456')
        ->and($customer->brandingResolved()['is_default'])->toBeFalse();
});

it('rejects an invalid brand colour', function () {
    ['user' => $user] = settingsSetup();

    Livewire::actingAs($user)->test(Settings::class)
        ->set('brandPrimary', 'notacolor')
        ->call('saveBranding')
        ->assertHasErrors(['brandPrimary']);
});

it('schedules package cancellation only with the correct typed confirmation', function () {
    $stripe = new FakeStripeClient;
    app()->instance(StripeClient::class, $stripe);

    ['user' => $user, 'customer' => $customer] = settingsSetup();
    $instance = $customer->instances()->first();

    // A YEARLY contract, because the assertion below is a date and a monthly
    // assumption would land eleven months early. This test used to assert only
    // that `service_ends_at` was not null — a column the code had just written,
    // which the broken monthly arithmetic satisfied exactly as well as the right
    // answer does.
    $contract = Subscription::factory()->plan('team', Subscription::TERM_YEARLY)->create([
        'customer_id' => $customer->id,
        'order_id' => $instance->order_id,
        'instance_id' => $instance->id,
        'stripe_subscription_id' => 'sub_settings',
        'current_period_start' => '2026-01-01 00:00:00',
        'current_period_end' => '2027-01-01 00:00:00',
    ]);

    // Wrong confirmation → no change, and Stripe is not told anything either.
    Livewire::actingAs($user)->test(ConfirmCancelPackage::class)
        ->set('confirmName', 'wrong')
        ->call('cancelPackage')
        ->assertHasErrors(['confirmName']);
    expect($instance->fresh()->status)->toBe('active')
        ->and($stripe->cancellations)->toBeEmpty();

    // Correct confirmation and a reason → scheduled. The reason is required:
    // a departure with none on it loses the one fact about it worth having,
    // and "we can ask later" means never.
    Livewire::actingAs($user)->test(ConfirmCancelPackage::class)
        ->set('confirmName', 'acme')
        ->call('cancelPackage')
        ->assertHasErrors(['reason']);

    expect($instance->fresh()->status)->toBe('active');

    Livewire::actingAs($user)->test(ConfirmCancelPackage::class)
        ->set('confirmName', 'acme')
        ->set('reason', 'too_expensive')
        ->set('exportWish', true)
        ->call('cancelPackage');

    $instance->refresh();

    expect($instance->status)->toBe('cancellation_scheduled')
        // The end of the term that was PAID FOR, stated as a date somebody chose.
        ->and($instance->service_ends_at->toDateTimeString())->toBe('2027-01-01 00:00:00');

    // The contract knows it was cancelled — and is still active, because until
    // the first of January this is a paying customer with a running cloud.
    expect($contract->fresh()->cancel_requested_at)->not->toBeNull()
        ->and($contract->fresh()->status)->toBe('active');

    // And Stripe was told to stop, at the period end and not now.
    expect($stripe->cancellations)->toHaveCount(1)
        ->and($stripe->cancellations[0]['subscription'])->toBe('sub_settings')
        ->and($stripe->cancellations[0]['when'])->toBe(StripeClient::CANCEL_AT_PERIOD_END);
});

it('blocks closing the account while a package is active', function () {
    ['user' => $user, 'customer' => $customer] = settingsSetup();

    Livewire::actingAs($user)->test(ConfirmCloseAccount::class)
        ->set('confirmWord', __('settings.close_keyword'))
        ->call('closeAccount')
        ->assertHasErrors(['confirmWord']);
    expect($customer->fresh()->closed_at)->toBeNull();
});

it('closes the account when no package is active', function () {
    ['user' => $user, 'customer' => $customer] = settingsSetup(withInstance: false);

    Livewire::actingAs($user)->test(ConfirmCloseAccount::class)
        ->set('confirmWord', __('settings.close_keyword'))
        ->call('closeAccount');

    expect($customer->fresh()->closed_at)->not->toBeNull();
});

// ---- Four tabs, and the choice in the address ----

it('opens on the customer own details and says so in the address', function () {
    // The page was one column 768px wide inside a 1240px shell, holding the
    // company address, the password, two-factor, the devices, the branding and
    // the contract — two thousand pixels of scroll, and finding a thing meant
    // knowing how far down it lived.
    $user = App\Models\User::factory()->create(['email_verified_at' => now()]);

    Livewire::actingAs($user)->test(App\Livewire\Settings::class)
        ->assertSet('tab', 'profile')
        ->assertSee(__('settings.company_title'))
        // Not the other tabs' content.
        ->assertDontSee(__('settings.branding_title'))
        ->assertDontSee(__('settings.package_title'));
});

it('lands on the tab the address names', function () {
    $user = App\Models\User::factory()->create(['email_verified_at' => now()]);

    Livewire::actingAs($user)->test(App\Livewire\Settings::class, ['tab' => 'branding'])
        ->assertSet('tab', 'branding')
        ->assertSee(__('settings.branding_title'))
        ->assertDontSee(__('settings.company_title'));
});

it('falls back to the first tab when the address names one nobody has', function () {
    // A tab name out of the URL is a string a stranger typed; the alternative
    // is a settings page with no section on it at all.
    $user = App\Models\User::factory()->create(['email_verified_at' => now()]);

    Livewire::actingAs($user)->test(App\Livewire\Settings::class, ['tab' => 'gibt-es-nicht'])
        ->assertSet('tab', 'profile')
        ->assertSee(__('settings.company_title'));
});

it('writes the tab into the query string, so a link can name one', function () {
    // history: true and no `except` — the parameter is there from the first
    // render. Hidden on the default, a link to this page could not say which
    // tab it meant unless the tab happened not to be the first.
    $component = Illuminate\Support\Facades\File::get(app_path('Livewire/Settings.php'));

    expect($component)->toContain('#[Url(history: true)]')
        // The property and its default, matched without spelling a PHP
        // variable inside a quoted string.
        ->and($component)->toContain('public string $tab')
        ->and($component)->toContain("= 'profile';")
        ->and($component)->not->toContain('except:');
});

it('keeps every tab reachable from the bar', function () {
    // The list IS the schema: it validates the query string, builds the bar and
    // decides which section renders.
    $user = App\Models\User::factory()->create(['email_verified_at' => now()]);
    $page = Livewire::actingAs($user)->test(App\Livewire\Settings::class);

    foreach (App\Livewire\Settings::TABS as $tab) {
        $page->assertSee(__('settings.tab.'.$tab));
    }
});

it('uses the width it is given', function () {
    // 768px in a 1240px shell left a third of the window empty beside a column
    // nobody could read the end of.
    expect(Illuminate\Support\Facades\File::get(resource_path('views/livewire/settings.blade.php')))
        ->not->toContain('max-w-3xl');
});

it('records why, and refuses "other" with nothing beside it', function () {
    ['user' => $user, 'customer' => $customer] = settingsSetup();
    $instance = $customer->instances()->where('status', 'active')->first();
    $contract = App\Models\Subscription::factory()->create([
        'customer_id' => $customer->id,
        'instance_id' => $instance->id,
        'status' => 'active',
    ]);

    Livewire::actingAs($user)->test(ConfirmCancelPackage::class)
        ->set('confirmName', $instance->subdomain)
        ->set('reason', 'other')
        ->call('cancelPackage')
        ->assertHasErrors(['note']);

    expect($instance->fresh()->status)->toBe('active');

    Livewire::actingAs($user)->test(ConfirmCancelPackage::class)
        ->set('confirmName', $instance->subdomain)
        ->set('reason', 'other')
        ->set('note', 'Wir ziehen intern um.')
        ->set('exportWish', false)
        ->call('cancelPackage');

    expect($contract->fresh()->cancel_reason)->toBe('other')
        ->and($contract->fresh()->cancel_reason_note)->toBe('Wir ziehen intern um.');
});

it('takes no reason it was not offered', function () {
    // The select is a select; the request is a request.
    ['user' => $user, 'customer' => $customer] = settingsSetup();
    $instance = $customer->instances()->where('status', 'active')->first();

    Livewire::actingAs($user)->test(ConfirmCancelPackage::class)
        ->set('confirmName', $instance->subdomain)
        ->set('reason', 'weil-ich-es-kann')
        ->call('cancelPackage')
        ->assertHasErrors(['reason']);

    expect($instance->fresh()->status)->toBe('active');
});

it('offers the withdrawal as a way out of the cancellation, while it is open', function () {
    // Withdrawing ends the service the same day and sends the WHOLE amount
    // back; cancelling keeps the term that was paid for. Somebody entitled to
    // the first should not lose it by taking the second unasked.
    ['user' => $user, 'customer' => $customer] = settingsSetup();
    $customer->update(['customer_type' => App\Models\Customer::TYPE_CONSUMER]);
    $instance = $customer->instances()->where('status', 'active')->first();
    App\Models\Subscription::factory()->create([
        'customer_id' => $customer->id,
        'instance_id' => $instance->id,
        'status' => 'active',
        'started_at' => now()->subDay(),
        'withdrawal_ends_at' => now()->addDays(13),
    ]);

    $page = Livewire::actingAs($user)->test(ConfirmCancelPackage::class)->html();

    expect($page)->toContain(__('withdrawal.instead_title'))
        ->and($page)->toContain('confirm-withdraw');
});

it('never offers a business the withdrawal it does not have', function () {
    ['user' => $user, 'customer' => $customer] = settingsSetup();
    $customer->update(['customer_type' => App\Models\Customer::TYPE_BUSINESS]);
    $instance = $customer->instances()->where('status', 'active')->first();
    App\Models\Subscription::factory()->create([
        'customer_id' => $customer->id,
        'instance_id' => $instance->id,
        'status' => 'active',
        'started_at' => now()->subDay(),
        'withdrawal_ends_at' => now()->addDays(13),
    ]);

    expect(Livewire::actingAs($user)->test(ConfirmCancelPackage::class)->html())
        ->not->toContain(__('withdrawal.instead_title'));
});

it('shows a consumer their withdrawal right, expired as well as open', function () {
    // It used to render only while the window was open, so once the fourteen
    // days ran out it vanished — indistinguishable from a feature that was never
    // built. Which is exactly how it was reported missing.
    ['user' => $user, 'customer' => $customer] = settingsSetup();
    $customer->update(['customer_type' => App\Models\Customer::TYPE_CONSUMER]);
    $instance = $customer->instances()->where('status', 'active')->first();

    $contract = App\Models\Subscription::factory()->create([
        'customer_id' => $customer->id,
        'instance_id' => $instance->id,
        'status' => 'active',
        'started_at' => now()->subDay(),
        'withdrawal_ends_at' => now()->addDays(13),
    ]);

    Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract'])
        ->assertSee(__('withdrawal.card_title'))
        ->assertSee(__('withdrawal.cta'));

    // Expired: still stated, with the reason, and no button.
    $contract->update(['withdrawal_ends_at' => now()->subDay()]);

    Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract'])
        ->assertSee(__('withdrawal.card_title'))
        ->assertDontSee(__('withdrawal.cta'));
});

it('never shows a business a right it does not have', function () {
    ['user' => $user, 'customer' => $customer] = settingsSetup();
    $customer->update(['customer_type' => App\Models\Customer::TYPE_BUSINESS]);
    $instance = $customer->instances()->where('status', 'active')->first();
    App\Models\Subscription::factory()->create([
        'customer_id' => $customer->id,
        'instance_id' => $instance->id,
        'status' => 'active',
        'started_at' => now()->subDay(),
        'withdrawal_ends_at' => now()->addDays(13),
    ]);

    Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract'])
        ->assertDontSee(__('withdrawal.card_title'));
});

it('offers the packages to somebody who has none, instead of a dead sentence', function () {
    $user = App\Models\User::factory()->create(['email_verified_at' => now()]);
    App\Models\Customer::factory()->create(['email' => $user->email, 'user_id' => $user->id]);

    Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract'])
        ->assertSee(__('settings.no_package'))
        ->assertSee(__('settings.to_packages'))
        // And nothing about withdrawing, because there is nothing to withdraw
        // from — which is what the owner ran into looking for the button.
        ->assertDontSee(__('withdrawal.card_title'));
});

it('builds every tab out of the same two pieces', function () {
    // Panels and rows, not a heap of cards of different heights: the group is
    // the card, the setting is a row inside it, and the label column is what
    // uses the width.
    $page = Illuminate\Support\Facades\File::get(resource_path('views/livewire/settings.blade.php'));

    expect(substr_count($page, '<x-ui.panel'))->toBeGreaterThanOrEqual(5)
        ->and(substr_count($page, '<x-ui.row'))->toBeGreaterThanOrEqual(12)
        // No hand-rolled card markup left behind.
        ->and($page)->not->toContain('rounded-lg border border-line bg-surface p-6 shadow-xs');
});
