<?php

declare(strict_types=1);

namespace Tests\Feature\Marketplace;

use App\Enums\ListingStatus;
use App\Enums\UserRole;
use App\Models\Domain;
use App\Models\Listing;
use App\Models\SellerProfile;
use App\Models\Tld;
use App\Models\User;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

/**
 * Ownership and state rules on domains and listings.
 *
 * The base OwnershipPolicy is already covered by PolicyTest; what is tested here
 * is what these two resources add to it -- the state rules. A listing that is
 * sold cannot be edited, a domain with a live listing cannot be discarded, and
 * an ownership proof is not something staff can perform on a seller's behalf.
 */
final class ListingAuthorizationTest extends TestCase
{
    use RefreshDatabase;

    /** @return array{0: User, 1: Domain} */
    private function ownerWithDomain(): array
    {
        $owner = User::factory()->create(['email_verified_at' => now()]);
        $domain = Domain::factory()->verified()->ownedBy($owner)->create();

        return [$owner, $domain->load('tld')];
    }

    private function listingFor(Domain $domain, string $state = 'active'): Listing
    {
        return Listing::factory()->forDomain($domain)->{$state}()->create();
    }

    #[Test]
    public function an_owner_may_read_and_update_their_own_listing(): void
    {
        [$owner, $domain] = $this->ownerWithDomain();
        $listing = $this->listingFor($domain);

        $this->assertTrue($owner->can('view', $listing));
        $this->assertTrue($owner->can('update', $listing));
    }

    /**
     * Someone else's listing answers as if it does not exist. On a marketplace,
     * "forbidden" confirms which names a given seller holds.
     */
    #[Test]
    public function a_stranger_gets_a_not_found_shaped_denial(): void
    {
        [, $domain] = $this->ownerWithDomain();
        $listing = $this->listingFor($domain);

        $stranger = User::factory()->create(['email_verified_at' => now()]);

        $response = Gate::forUser($stranger)->inspect('update', $listing);

        $this->assertTrue($response->denied());
        $this->assertSame(404, $response->status());
    }

    /**
     * A sold listing is the record of what was agreed. Editing its price
     * afterwards would rewrite the terms of a completed sale.
     */
    #[Test]
    public function a_sold_listing_cannot_be_edited(): void
    {
        [$owner, $domain] = $this->ownerWithDomain();
        $listing = $this->listingFor($domain, 'sold');

        $this->assertFalse($owner->can('update', $listing));
    }

    /**
     * Editing a suspended listing is how a seller would work around
     * enforcement, so it is refused even to the owner.
     */
    #[Test]
    public function a_suspended_listing_cannot_be_edited_by_its_owner(): void
    {
        [$owner, $domain] = $this->ownerWithDomain();
        $listing = $this->listingFor($domain, 'suspended');

        $this->assertFalse($owner->can('update', $listing));
    }

    /**
     * Taking your own name off the market is the one action standing should not
     * block: forcing a seller to keep an offer live is worse than letting a
     * suspended one retract it.
     */
    #[Test]
    public function withdrawing_stays_available_even_to_a_seller_who_cannot_transact(): void
    {
        [, $domain] = $this->ownerWithDomain();
        $listing = $this->listingFor($domain);

        // Unverified email: canTransact() is false, canAuthenticate() is true.
        $owner = $listing->user()->firstOrFail();
        $owner->forceFill(['email_verified_at' => null])->save();

        $this->assertFalse($owner->canTransact());
        $this->assertTrue($owner->can('withdraw', $listing));
        $this->assertFalse($owner->can('update', $listing));
    }

    #[Test]
    public function an_already_closed_listing_cannot_be_withdrawn_again(): void
    {
        [$owner, $domain] = $this->ownerWithDomain();

        $this->assertFalse($owner->can('withdraw', $this->listingFor($domain, 'sold')));
    }

    /**
     * A published listing is part of the marketplace's history: offers may
     * reference it and a dispute may need it.
     */
    #[Test]
    public function a_published_listing_can_be_withdrawn_but_not_deleted(): void
    {
        [$owner, $domain] = $this->ownerWithDomain();
        $published = $this->listingFor($domain);

        $this->assertFalse($owner->can('delete', $published));
        $this->assertTrue($owner->can('withdraw', $published));
    }

    #[Test]
    public function an_unpublished_draft_can_be_deleted(): void
    {
        [$owner, $domain] = $this->ownerWithDomain();
        $draft = Listing::factory()->forDomain($domain)->create();

        $this->assertTrue($owner->can('delete', $draft));
    }

    #[Test]
    public function publishing_requires_a_seller_who_may_list(): void
    {
        [$owner, $domain] = $this->ownerWithDomain();
        $listing = Listing::factory()->forDomain($domain)->create();
        $listing->load('seller');

        $this->assertTrue($owner->can('publish', $listing));

        $listing->seller->forceFill([
            'listing_suspended_at' => now(),
            'listing_suspension_reason' => 'Under review.',
        ])->save();

        $this->assertFalse($owner->fresh()->can('publish', $listing->fresh()->load('seller')));
    }

    /**
     * The floor is the seller's, not the buyer's. Support can read it for a
     * dispute; nobody else can.
     */
    #[Test]
    public function the_offer_floor_is_readable_by_the_owner_and_support_only(): void
    {
        [$owner, $domain] = $this->ownerWithDomain();
        $listing = $this->listingFor($domain);

        $stranger = User::factory()->create(['email_verified_at' => now()]);
        $support = User::factory()->create(['role' => UserRole::Support, 'email_verified_at' => now()]);

        $this->assertTrue($owner->can('viewMinimumOffer', $listing));
        $this->assertTrue($support->can('viewMinimumOffer', $listing));
        $this->assertFalse($stranger->can('viewMinimumOffer', $listing));
    }

    #[Test]
    public function suspending_a_listing_requires_admin_not_support(): void
    {
        [, $domain] = $this->ownerWithDomain();
        $listing = $this->listingFor($domain);

        $support = User::factory()->create(['role' => UserRole::Support, 'email_verified_at' => now()]);
        $admin = User::factory()->create(['role' => UserRole::Admin, 'email_verified_at' => now()]);

        $this->assertFalse($support->can('suspend', $listing));
        $this->assertTrue($admin->can('suspend', $listing));
    }

    /**
     * Deleting the inventory row under a live offer would leave a buyer
     * negotiating over a name the seller has discarded.
     */
    #[Test]
    public function a_domain_with_an_open_listing_cannot_be_deleted(): void
    {
        [$owner, $domain] = $this->ownerWithDomain();
        $listing = $this->listingFor($domain);

        $this->assertFalse($owner->can('delete', $domain));

        // Withdrawing frees the domain's open slot, and with it the domain.
        $listing->status = ListingStatus::Withdrawn;
        $listing->save();

        $this->assertTrue($owner->can('delete', $domain));
    }

    /**
     * Support may read a domain for dispute handling but must never be able to
     * assert ownership on a seller's behalf -- that would make the
     * marketplace's central guarantee an employee's word rather than the DNS's.
     */
    #[Test]
    public function ownership_proof_is_the_owners_alone(): void
    {
        [$owner, $domain] = $this->ownerWithDomain();

        $support = User::factory()->create(['role' => UserRole::Support, 'email_verified_at' => now()]);
        $superAdmin = User::factory()->create(['role' => UserRole::SuperAdmin, 'email_verified_at' => now()]);

        $this->assertTrue($owner->can('verify', $domain));
        $this->assertFalse($support->can('verify', $domain));
        $this->assertFalse($superAdmin->can('verify', $domain));

        // Reading is a different question from proving.
        $this->assertTrue($support->can('view', $domain));
    }

    /**
     * The verification token is what proves control, so handing it to anyone
     * else -- including staff -- would make the proof transferable.
     */
    #[Test]
    public function the_verification_token_is_not_readable_by_staff(): void
    {
        [$owner, $domain] = $this->ownerWithDomain();
        $support = User::factory()->create(['role' => UserRole::Support, 'email_verified_at' => now()]);

        $this->assertTrue($owner->can('viewVerificationToken', $domain));
        $this->assertFalse($support->can('viewVerificationToken', $domain));
    }

    /**
     * The database enforces one open listing per domain, so a second cannot be
     * created even if application logic ever let it through.
     */
    #[Test]
    public function a_domain_cannot_hold_two_open_listings(): void
    {
        [, $domain] = $this->ownerWithDomain();
        $this->listingFor($domain);

        $this->expectException(UniqueConstraintViolationException::class);

        $this->listingFor($domain);
    }

    /**
     * Relisting is the normal case and must keep working: a seller may withdraw
     * and offer the same name again any number of times.
     */
    #[Test]
    public function a_withdrawn_listing_frees_the_domain_to_be_relisted(): void
    {
        [, $domain] = $this->ownerWithDomain();

        $first = $this->listingFor($domain);
        $first->status = ListingStatus::Withdrawn;
        $first->save();

        $second = $this->listingFor($domain);

        $this->assertTrue($second->exists);
        $this->assertNotSame($first->getKey(), $second->getKey());
    }

    /**
     * An unproven name must not become visible to buyers. This is the check the
     * public browse surface then relies on.
     */
    #[Test]
    public function an_unverified_domain_cannot_be_published(): void
    {
        $owner = User::factory()->create(['email_verified_at' => now()]);
        $domain = Domain::factory()->ownedBy($owner)->create();
        SellerProfile::factory()->create(['user_id' => $owner->getKey()]);

        $listing = Listing::factory()->forDomain($domain)->create();
        $listing->load('domain.tld');

        $this->expectException(\DomainException::class);

        $listing->publish();
    }

    /**
     * A TLD with no registrar-to-registrar transfer path has no viable handover,
     * so listing it would promise something undeliverable.
     */
    #[Test]
    public function a_domain_on_an_untransferable_tld_cannot_be_published(): void
    {
        $owner = User::factory()->create(['email_verified_at' => now()]);

        Tld::factory()->untransferable()->create(['tld' => 'np']);

        $domain = Domain::factory()->verified()->ownedBy($owner)->named('example.np')->create();
        SellerProfile::factory()->create(['user_id' => $owner->getKey()]);

        $listing = Listing::factory()->forDomain($domain)->create();
        $listing->load('domain.tld');

        $this->expectException(\DomainException::class);

        $listing->publish();
    }
}
