<?php

namespace Tests\Feature;

use App\Domain\Attendance\Enums\AttendanceStatus;
use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\Compliance\Enums\ConsentType;
use App\Domain\Compliance\Models\Consent;
use App\Domain\Compliance\Services\AccountDeletionService;
use App\Domain\Compliance\Services\ConsentService;
use App\Domain\Identity\Services\GuardianResolver;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use App\Domain\Shared\Models\Academy;
use App\Domain\Shared\Models\ServiceRequest;
use App\Domain\Shared\Services\ServiceRequestService;
use App\Domain\Training\Enums\SessionStatus;
use App\Domain\Training\Models\TrainingSession;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

/**
 * Requests, excuses, consent and deletion, against a restored real tenant.
 *
 * The thing under test is not "does the status change" — it is **does approving
 * a request actually do the thing**. Approval used to flip a column and nothing
 * else: a member was told their subscription was frozen and it was not.
 *
 *   DB_CONNECTION=pgsql DB_DATABASE=oc_sport_test ./vendor/bin/phpunit --filter ServiceRequestEffectTest
 */
class ServiceRequestEffectTest extends TestCase
{
    private Academy $academy;

    private User $member;

    private User $staff;

    private Participant $participant;

    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);

        DB::beginTransaction();

        $resolver = app(GuardianResolver::class);

        foreach (User::withoutGlobalScopes()->whereNotNull('person_id')->limit(200)->get() as $candidate) {
            $ids = $resolver->participantIdsFor($candidate);

            if ($ids !== []) {
                $this->member = $candidate;
                $this->participant = Participant::findOrFail($ids[0]);
                break;
            }
        }

        if (! isset($this->member)) {
            $this->markTestSkipped('No account on this tenant speaks for a participant.');
        }

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

        $this->participant->update(['status' => 'active']);
    }

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

        parent::tearDown();
    }

    // ---- requests ----------------------------------------------------------

    public function test_a_member_cannot_open_a_request_for_someone_elses_child(): void
    {
        $foreign = Participant::whereNotIn('id', app(GuardianResolver::class)->participantIdsFor($this->member))
            ->firstOrFail();

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

        app(ServiceRequestService::class)->open([
            'type' => 'freeze',
            'participant_id' => $foreign->id,
            'reason' => 'محاولة غير مصرح بها',
        ], $this->member);
    }

    public function test_approving_a_freeze_actually_freezes_the_participant(): void
    {
        // The whole point. `grep -rln ServiceRequest` found no admin screen and
        // no call to ParticipantService::freeze() anywhere — approval changed a
        // column and the subscription kept running.
        $request = app(ServiceRequestService::class)->open([
            'type' => 'freeze',
            'participant_id' => $this->participant->id,
            'reason' => 'سفر خارج البلاد',
        ], $this->member);

        app(ServiceRequestService::class)->approve($request, $this->staff);

        $status = $this->participant->refresh()->status;

        $this->assertSame('frozen', $status instanceof \BackedEnum ? $status->value : $status);
        $this->assertNotNull($request->refresh()->effect_applied_at);
    }

    public function test_two_administrators_cannot_both_approve(): void
    {
        $request = app(ServiceRequestService::class)->open([
            'type' => 'other',
            'participant_id' => $this->participant->id,
            'reason' => 'استفسار',
        ], $this->member);

        app(ServiceRequestService::class)->approve($request, $this->staff);

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

        app(ServiceRequestService::class)->approve($request->refresh(), $this->staff);
    }

    // ---- excuses -----------------------------------------------------------

    public function test_an_excuse_for_a_session_outside_the_members_groups_is_refused(): void
    {
        // The deleted endpoint never checked this at all, which is what let a
        // member excuse himself from somebody else's class.
        $groupIds = $this->participant->activeEnrollments()->pluck('training_group_id')->all();

        $foreignSession = TrainingSession::whereNotIn('training_group_id', $groupIds ?: [0])->first();

        if (! $foreignSession) {
            $this->markTestSkipped('This tenant has no session outside the member\'s groups.');
        }

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

        app(ServiceRequestService::class)->open([
            'type' => 'excuse',
            'participant_id' => $this->participant->id,
            'training_session_id' => $foreignSession->id,
            'reason' => 'مرض',
        ], $this->member);
    }

    public function test_an_approved_excuse_marks_attendance_through_the_one_write_path(): void
    {
        $session = $this->aSessionForTheMember();

        $record = AttendanceRecord::create([
            'academy_id' => $this->academy->id,
            'training_session_id' => $session->id,
            'subject_type' => Participant::class,
            'subject_id' => $this->participant->id,
            'status' => AttendanceStatus::Expected,
        ]);

        $request = app(ServiceRequestService::class)->open([
            'type' => 'excuse',
            'participant_id' => $this->participant->id,
            'training_session_id' => $session->id,
            'reason' => 'مرض',
        ], $this->member);

        app(ServiceRequestService::class)->approve($request, $this->staff);

        $record->refresh();

        $this->assertSame(AttendanceStatus::Excused, $record->status);
        // A marker, which the deleted endpoint never recorded.
        $this->assertSame($this->staff->id, (int) $record->marked_by);
    }

    public function test_an_excuse_does_not_overwrite_a_coachs_observation(): void
    {
        $session = $this->aSessionForTheMember();

        AttendanceRecord::create([
            'academy_id' => $this->academy->id,
            'training_session_id' => $session->id,
            'subject_type' => Participant::class,
            'subject_id' => $this->participant->id,
            'status' => AttendanceStatus::Present,
            'marked_by' => $this->staff->id,
            'marked_at' => now(),
        ]);

        $request = app(ServiceRequestService::class)->open([
            'type' => 'excuse',
            'participant_id' => $this->participant->id,
            'training_session_id' => $session->id,
            'reason' => 'مرض',
        ], $this->member);

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

        app(ServiceRequestService::class)->approve($request, $this->staff);
    }

    public function test_a_failed_effect_leaves_the_request_pending(): void
    {
        // An excuse with no attendance row cannot take effect. The approval
        // must not stand: "approved, and nothing happened" is worse than
        // "still pending".
        $session = $this->aSessionForTheMember();

        $request = app(ServiceRequestService::class)->open([
            'type' => 'excuse',
            'participant_id' => $this->participant->id,
            'training_session_id' => $session->id,
            'reason' => 'مرض',
        ], $this->member);

        try {
            app(ServiceRequestService::class)->approve($request, $this->staff);
            $this->fail('approval should have failed with no attendance record');
        } catch (DomainException) {
            // expected
        }

        $this->assertSame('pending', ServiceRequest::withoutGlobalScopes()->find($request->id)->status);
    }

    // ---- consent -----------------------------------------------------------

    public function test_consent_is_append_only(): void
    {
        $consent = app(ConsentService::class)->record(
            ConsentType::PhotoMedia, true, $this->member, $this->participant
        );

        $this->expectException(\Illuminate\Database\QueryException::class);

        DB::table('consents')->where('id', $consent->id)->update(['granted' => false]);
    }

    public function test_withdrawing_consent_is_a_new_row_and_the_latest_answer_wins(): void
    {
        $service = app(ConsentService::class);

        $service->record(ConsentType::Marketing, true, $this->member, $this->participant);
        $service->record(ConsentType::Marketing, false, $this->member, $this->participant);

        $this->assertFalse($service->hasGranted($this->member, ConsentType::Marketing, $this->participant));

        // Both rows survive: the history is what makes it evidence.
        $this->assertSame(2, Consent::withoutGlobalScopes()
            ->where('participant_id', $this->participant->id)
            ->where('type', ConsentType::Marketing->value)
            ->count());
    }

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

        app(ConsentService::class)->recordMany(
            [ConsentType::Terms->value => false],
            $this->member,
            $this->participant,
        );
    }

    public function test_consent_recorded_against_an_older_document_version_is_not_a_current_answer(): void
    {
        app(ConsentService::class)->record(ConsentType::Marketing, true, $this->member, $this->participant);

        config(['compliance.consent_version' => '99.0']);

        $this->assertFalse(
            app(ConsentService::class)->hasGranted($this->member, ConsentType::Marketing, $this->participant),
            'a consent to text the member has never seen is not consent'
        );
    }

    // ---- deletion ----------------------------------------------------------

    public function test_deletion_is_blocked_while_money_is_owed(): void
    {
        \App\Domain\Financial\Models\Invoice::create([
            'academy_id' => $this->academy->id,
            'number' => 'INV-DEL-TEST',
            'type' => 'standard',
            'status' => 'sent',
            'billable_type' => Participant::class,
            'billable_id' => $this->participant->id,
            'total_amount' => 10000,
            'due_amount' => 10000,
            'issue_date' => now()->toDateString(),
            'due_date' => now()->toDateString(),
        ]);

        $blockers = app(AccountDeletionService::class)->blockers($this->member);

        $this->assertNotEmpty($blockers);
        $this->assertStringContainsString('مستحق', implode(' ', $blockers));
    }

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

        app(AccountDeletionService::class)->request($this->member, null, 'not-the-password');
    }

    public function test_the_export_contains_the_account_and_its_members(): void
    {
        $export = app(AccountDeletionService::class)->export($this->member);

        $this->assertArrayHasKey('account', $export);
        $this->assertArrayHasKey('members', $export);
        $this->assertArrayHasKey('invoices', $export);
        $this->assertNotEmpty($export['members']);
    }

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

    private function aSessionForTheMember(): TrainingSession
    {
        $groupId = $this->participant->activeEnrollments()->value('training_group_id');

        if (! $groupId) {
            $this->markTestSkipped('The member has no active enrolment on this tenant.');
        }

        return TrainingSession::create([
            'academy_id' => $this->academy->id,
            'training_group_id' => $groupId,
            'session_date' => now()->addDay()->toDateString(),
            'start_time' => '17:00:00',
            'end_time' => '18:00:00',
            'status' => SessionStatus::Scheduled,
        ]);
    }
}
