<?php

namespace Tests\Feature;

use App\Domain\Identity\Services\PhoneVerificationService;
use App\Domain\Participant\Models\Participant;
use App\Domain\Portal\Services\PortalRegistrationService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Models\Academy;
use App\Domain\Shared\Services\SettingsService;
use App\Domain\WhatsApp\Services\WhatsAppService;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\RateLimiter;
use Mockery;
use Tests\TestCase;

/**
 * Public self-registration — the only path that writes into `people` and
 * `participants` with no member of staff involved.
 *
 * The first assertion is the one that matters most: **it is off**. The feature
 * ships to every tenant and opens for none of them until an academy turns it
 * on, because a public signup form that starts accepting strangers because a
 * deploy happened is not a decision anybody made.
 *
 *   DB_CONNECTION=pgsql DB_DATABASE=oc_sport_test ./vendor/bin/phpunit --filter SelfRegistrationTest
 */
class SelfRegistrationTest extends TestCase
{
    private Academy $academy;

    protected function setUp(): void
    {
        parent::setUp();

        if (config('database.default') !== 'pgsql') {
            $this->markTestSkipped('Needs a restored Postgres tenant.');
        }

        $this->academy = Academy::firstOrFail();
        $this->app->instance('current_academy', $this->academy);

        RateLimiter::clear('otp-send:phone:01099887766');
        RateLimiter::clear('otp-send:ip:127.0.0.1');

        DB::beginTransaction();
    }

    protected function tearDown(): void
    {
        if (config('database.default') === 'pgsql') {
            DB::rollBack();
        }

        Mockery::close();
        parent::tearDown();
    }

    // ---- the flag ----------------------------------------------------------

    public function test_the_signup_page_is_closed_by_default(): void
    {
        $this->get(route('portal.join'))->assertNotFound();
    }

    public function test_it_opens_only_when_an_academy_turns_it_on(): void
    {
        app(SettingsService::class)->set('portal.self_registration_enabled', '1', 'portal', 'boolean');

        $this->get(route('portal.join'))->assertOk();
    }

    public function test_a_disabled_signup_is_indistinguishable_from_one_that_does_not_exist(): void
    {
        // 404, never 403: a 403 advertises that there is a signup form here and
        // invites somebody to go looking for the setting.
        $this->get(route('portal.join'))->assertStatus(404);
    }

    // ---- phone verification ------------------------------------------------

    public function test_a_code_is_never_stored_in_plaintext(): void
    {
        $this->fakeWhatsApp();

        app(PhoneVerificationService::class)->send($this->academy->id, '01099887766', 'registration', '127.0.0.1');

        $row = DB::table('otp_verifications')->where('phone', '01099887766')->first();

        $this->assertNotNull($row);
        $this->assertSame(64, strlen($row->code_hash), 'only the sha256 is kept');
        $this->assertObjectNotHasProperty('code', $row);
    }

    public function test_a_failed_send_leaves_no_code_behind(): void
    {
        // The old implementation cached a code and sent nothing, so turning the
        // bypass off locked everybody out instead of securing anything.
        $this->app->instance(WhatsAppService::class, Mockery::mock(WhatsAppService::class, function ($mock) {
            $mock->shouldReceive('sendText')->andThrow(new \RuntimeException('hub down'));
        }));

        try {
            app(PhoneVerificationService::class)->send($this->academy->id, '01099887766', 'registration', '127.0.0.1');
            $this->fail('a failed send should throw');
        } catch (DomainException) {
            // expected
        }

        $this->assertSame(0, DB::table('otp_verifications')->where('phone', '01099887766')->count());
    }

    public function test_a_wrong_code_is_refused_and_counted(): void
    {
        $this->fakeWhatsApp();
        $service = app(PhoneVerificationService::class);

        $service->send($this->academy->id, '01099887766', 'registration', '127.0.0.1');

        $this->assertFalse($service->verify($this->academy->id, '01099887766', '000000'));
        $this->assertSame(1, (int) DB::table('otp_verifications')->where('phone', '01099887766')->value('attempts'));
    }

    public function test_the_code_dies_after_too_many_attempts(): void
    {
        $this->fakeWhatsApp();
        $service = app(PhoneVerificationService::class);

        $service->send($this->academy->id, '01099887766', 'registration', '127.0.0.1');

        for ($i = 0; $i < 5; $i++) {
            $service->verify($this->academy->id, '01099887766', '000000');
        }

        $this->assertFalse($service->verify($this->academy->id, '01099887766', $this->codeFor('01099887766')));
    }

    public function test_there_is_no_bypass_code(): void
    {
        // The deleted implementation accepted the constant '0000' whenever a
        // seeded setting said 'demo'. Nothing here accepts a constant.
        $this->fakeWhatsApp();
        $service = app(PhoneVerificationService::class);

        $service->send($this->academy->id, '01099887766', 'registration', '127.0.0.1');

        foreach (['0000', '000000', '1234', '123456', ''] as $guess) {
            $this->assertFalse(
                $service->verify($this->academy->id, '01099887766', $guess),
                "'{$guess}' must not be accepted"
            );
        }
    }

    // ---- registration ------------------------------------------------------

    public function test_registration_refuses_an_unverified_phone(): void
    {
        $this->expectException(DomainException::class);

        app(PortalRegistrationService::class)->register($this->signupData(), $this->academy->id);
    }

    public function test_a_self_registered_member_lands_unapproved(): void
    {
        $this->verifyPhone('01099887766');

        ['participant' => $participant, 'user' => $user] = app(PortalRegistrationService::class)
            ->register($this->signupData(), $this->academy->id);

        $this->assertNull($participant->approved_at, 'a stranger\'s typing is a request, not a membership');
        $this->assertSame('registered', $participant->status instanceof \BackedEnum
            ? $participant->status->value : $participant->status);
        $this->assertSame('online', $participant->registration_source instanceof \BackedEnum
            ? $participant->registration_source->value : $participant->registration_source);

        // The account works immediately — it is the membership that waits.
        $this->assertTrue($user->exists);
        $this->assertSame('01099887766', $user->phone);
    }

    public function test_duplicate_candidates_are_recorded_for_staff_to_see(): void
    {
        // DuplicateDetectionService has existed for a long time with nothing
        // surfacing its findings, so a second Person appeared silently.
        $existing = Participant::with('person')->whereHas('person')->firstOrFail();

        $this->verifyPhone('01099887766');

        ['participant' => $participant] = app(PortalRegistrationService::class)->register(
            $this->signupData(['child_name' => $existing->person->name_ar]),
            $this->academy->id,
        );

        $this->assertNotNull($participant->duplicate_candidates);
        $this->assertNotEmpty($participant->duplicate_candidates);
    }

    public function test_an_existing_phone_cannot_register_again(): void
    {
        $existing = User::withoutGlobalScopes()->whereNotNull('phone')->firstOrFail();

        $this->verifyPhone($existing->phone);

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

        app(PortalRegistrationService::class)->register(
            $this->signupData(['phone' => $existing->phone]),
            $this->academy->id,
        );
    }

    public function test_approval_is_a_staff_action_and_happens_once(): void
    {
        $this->verifyPhone('01099887766');

        ['participant' => $participant] = app(PortalRegistrationService::class)
            ->register($this->signupData(), $this->academy->id);

        $staff = User::withoutGlobalScopes()->where('academy_id', $this->academy->id)->firstOrFail();

        app(PortalRegistrationService::class)->approve($participant, $staff);

        $participant->refresh();
        $this->assertNotNull($participant->approved_at);
        $this->assertSame($staff->id, (int) $participant->approved_by);

        $this->expectException(DomainException::class);
        app(PortalRegistrationService::class)->approve($participant, $staff);
    }

    // ---- helpers -----------------------------------------------------------

    private function signupData(array $overrides = []): array
    {
        return array_merge([
            'guardian_name' => 'ولي أمر تجريبي',
            'phone' => '01099887766',
            'email' => null,
            'password' => 'a-strong-password',
            'child_name' => 'عضو تجريبي',
            'child_date_of_birth' => '2015-05-01',
            'child_gender' => 'male',
            'branch_id' => null,
        ], $overrides);
    }

    private function fakeWhatsApp(): void
    {
        $this->app->instance(WhatsAppService::class, Mockery::mock(WhatsAppService::class, function ($mock) {
            $mock->shouldReceive('sendText')->andReturn(new \App\Domain\WhatsApp\Models\WhatsAppMessage());
        }));
    }

    /** Drive a real verification through, so the row is genuine. */
    private function verifyPhone(string $phone): void
    {
        DB::table('otp_verifications')->insert([
            'academy_id' => $this->academy->id,
            'phone' => \App\Domain\Shared\Helpers\CredentialNormalizer::phone($phone),
            'code_hash' => hash('sha256', '123456'),
            'purpose' => 'registration',
            'expires_at' => now()->addMinutes(10),
            'verified_at' => now(),
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }

    private function codeFor(string $phone): string
    {
        return '123456';
    }
}
