<?php

namespace Tests\Feature\Commissions;

use App\Domain\Agents\Models\Agent;
use App\Domain\Applicants\Models\Applicant;
use App\Domain\Commissions\Enums\CommissionStatus;
use App\Domain\Commissions\Enums\CommissionTrigger;
use App\Domain\Commissions\Models\Commission;
use App\Domain\Commissions\Models\CommissionRule;
use App\Domain\Commissions\Services\CommissionEngine;
use App\Domain\Identity\Models\User;
use App\Domain\Organization\Models\Branch;
use App\Domain\Organization\Models\Company;
use Database\Seeders\PermissionSeeder;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use RuntimeException;
use Tests\TestCase;

class CommissionEngineTest extends TestCase
{
    use RefreshDatabase;

    private Company $company;

    private Branch $branch;

    private Agent $agent;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed([PermissionSeeder::class, RoleSeeder::class]);

        $this->company = Company::factory()->create();
        $this->branch = Branch::factory()->for($this->company)->create();
        $this->agent = Agent::factory()->for($this->company)->create([
            'branch_id' => $this->branch->id,
            'status' => 'active',
        ]);
    }

    private function user(string $role): User
    {
        return tap(User::factory()->create([
            'company_id' => $this->company->id,
            'branch_id' => $this->branch->id,
        ]))->assignRole($role);
    }

    private function applicant(array $attributes = []): Applicant
    {
        return Applicant::factory()->for($this->company)->create(array_merge([
            'branch_id' => $this->branch->id,
            'agent_id' => $this->agent->id,
            'destination_country' => 'SA',
        ], $attributes));
    }

    /** @param array<string, mixed> $attributes */
    private function rule(array $attributes = []): CommissionRule
    {
        return CommissionRule::create(array_merge([
            'code' => 'reg-bonus',
            'name' => 'Registration bonus',
            'company_id' => $this->company->id,
            'trigger_event' => 'applicant_verified',
            'calculation_basis' => 'fixed',
            'currency' => 'BDT',
            'fixed_amount' => 50000,
            'priority' => 100,
            'is_active' => true,
        ], $attributes));
    }

    public function test_verifying_an_applicant_accrues_the_registration_bonus(): void
    {
        $this->rule();
        $applicant = $this->applicant();

        $this->actingAs($this->user('branch-manager'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/verify")
            ->assertOk();

        $commission = Commission::where('agent_id', $this->agent->id)->firstOrFail();

        $this->assertSame(50000, (int) $commission->commission_amount);
        $this->assertSame(CommissionStatus::Accrued, $commission->status);
        $this->assertSame('reg-bonus', $commission->calculation_breakdown['rule']);
    }

    public function test_an_applicant_without_an_agent_earns_nobody_anything(): void
    {
        $this->rule();
        $applicant = $this->applicant(['agent_id' => null]);

        $this->actingAs($this->user('branch-manager'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/verify")
            ->assertOk();

        $this->assertSame(0, Commission::count());
    }

    public function test_the_same_event_cannot_be_paid_for_twice(): void
    {
        $this->rule();
        $applicant = $this->applicant();
        $engine = app(CommissionEngine::class);

        // Replaying the same event — a retried job, a double callback — must
        // not earn the agent a second time (§13A requirement 2).
        $engine->handle(CommissionTrigger::ApplicantVerified, $applicant, $applicant);
        $engine->handle(CommissionTrigger::ApplicantVerified, $applicant, $applicant);

        $this->assertSame(1, Commission::where('applicant_id', $applicant->id)->count());
    }

    public function test_a_rule_for_another_destination_does_not_fire(): void
    {
        $this->rule(['destination_country' => 'AE']);
        $applicant = $this->applicant(['destination_country' => 'SA']);

        $this->actingAs($this->user('branch-manager'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/verify")
            ->assertOk();

        $this->assertSame(0, Commission::count());
    }

    public function test_an_expired_scheme_does_not_fire(): void
    {
        $this->rule([
            'effective_from' => now()->subYear()->toDateString(),
            'effective_to' => now()->subMonth()->toDateString(),
        ]);

        $applicant = $this->applicant();

        $this->actingAs($this->user('branch-manager'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/verify")
            ->assertOk();

        $this->assertSame(0, Commission::count());
    }

    public function test_the_first_matching_rule_wins_unless_it_is_stackable(): void
    {
        $this->rule(['code' => 'narrow', 'priority' => 10, 'fixed_amount' => 30000]);
        $this->rule(['code' => 'broad', 'priority' => 50, 'fixed_amount' => 70000]);

        $applicant = $this->applicant();

        $this->actingAs($this->user('branch-manager'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/verify")
            ->assertOk();

        $commissions = Commission::all();
        $this->assertCount(1, $commissions);
        $this->assertSame(30000, (int) $commissions->first()->commission_amount);
    }

    public function test_stackable_rules_both_fire(): void
    {
        $this->rule(['code' => 'base', 'priority' => 10, 'fixed_amount' => 30000, 'is_stackable' => true]);
        $this->rule(['code' => 'campaign', 'priority' => 20, 'fixed_amount' => 10000]);

        $applicant = $this->applicant();

        $this->actingAs($this->user('branch-manager'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/verify")
            ->assertOk();

        $this->assertSame(40000, (int) Commission::sum('commission_amount'));
    }

    public function test_a_collection_commission_is_a_percentage_of_the_payment(): void
    {
        $this->rule([
            'code' => 'collection',
            'trigger_event' => 'payment_confirmed',
            'calculation_basis' => 'percent_of_collection',
            'fixed_amount' => 0,
            // 2% expressed in basis points, so no float is ever involved.
            'rate_basis_points' => 200,
        ]);

        $applicant = $this->applicant();

        $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/payments", [
                'amount' => 150000,
                'currency' => 'BDT',
                'method' => 'cash',
            ])->assertCreated();

        $this->assertSame(3000, (int) Commission::sum('commission_amount'));
    }

    public function test_a_cap_limits_what_a_percentage_rule_can_earn(): void
    {
        $this->rule([
            'code' => 'collection',
            'trigger_event' => 'payment_confirmed',
            'calculation_basis' => 'percent_of_collection',
            'fixed_amount' => 0,
            'rate_basis_points' => 1000,
            'maximum_amount' => 5000,
        ]);

        $applicant = $this->applicant();

        $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/payments", [
                'amount' => 200000,
                'currency' => 'BDT',
                'method' => 'cash',
            ])->assertCreated();

        $commission = Commission::firstOrFail();
        $this->assertSame(5000, (int) $commission->commission_amount);
        $this->assertTrue($commission->calculation_breakdown['cap_applied']);
    }

    public function test_an_unverified_deposit_earns_nothing_until_it_is_verified(): void
    {
        $this->rule([
            'code' => 'collection',
            'trigger_event' => 'payment_confirmed',
            'calculation_basis' => 'percent_of_collection',
            'fixed_amount' => 0,
            'rate_basis_points' => 200,
        ]);

        $applicant = $this->applicant();

        $payment = $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/payments", [
                'amount' => 100000,
                'currency' => 'BDT',
                'method' => 'bank_deposit',
                'transaction_reference' => 'DEP-COM-1',
            ])->assertCreated()->json('data.id');

        $this->assertSame(0, Commission::count());

        $this->actingAs($this->user('accounts-manager'))
            ->postJson("/api/v1/payments/{$payment}/verify")
            ->assertOk();

        $this->assertSame(2000, (int) Commission::sum('commission_amount'));
    }

    public function test_a_clawback_leaves_the_original_untouched(): void
    {
        $this->rule();
        $applicant = $this->applicant();

        $this->actingAs($this->user('branch-manager'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/verify")
            ->assertOk();

        $original = Commission::firstOrFail();

        $this->actingAs($this->user('director'))
            ->postJson("/api/v1/commissions/{$original->uuid}/reverse", [
                'reason' => 'Applicant turned out to be a duplicate.',
            ])
            ->assertOk()
            ->assertJsonPath('data.is_reversal', true);

        $original->refresh();
        $this->assertSame(CommissionStatus::Reversed, $original->status);
        $this->assertSame(50000, (int) $original->commission_amount);
        $this->assertSame(2, Commission::count());
    }

    public function test_a_commission_amount_cannot_be_edited(): void
    {
        $this->rule();
        $applicant = $this->applicant();

        $this->actingAs($this->user('branch-manager'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/verify")
            ->assertOk();

        $commission = Commission::firstOrFail();

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

        $commission->commission_amount = 999999;
        $commission->save();
    }

    public function test_a_percentage_rule_cannot_hang_off_an_event_with_no_amount(): void
    {
        $this->actingAs($this->user('director'))
            ->postJson('/api/v1/commission-rules', [
                'code' => 'nonsense',
                'name' => 'Percentage of a verification',
                'trigger_event' => 'applicant_verified',
                'calculation_basis' => 'percent_of_collection',
                'currency' => 'BDT',
                'rate_basis_points' => 200,
            ])
            ->assertStatus(422);
    }

    public function test_an_agent_only_sees_their_own_commission(): void
    {
        $this->rule();
        $mine = $this->applicant();

        $otherAgent = Agent::factory()->for($this->company)->create(['branch_id' => $this->branch->id]);
        $theirs = $this->applicant(['agent_id' => $otherAgent->id]);

        $manager = $this->user('branch-manager');
        $this->actingAs($manager)->postJson("/api/v1/applicants/{$mine->uuid}/verify")->assertOk();
        $this->actingAs($manager)->postJson("/api/v1/applicants/{$theirs->uuid}/verify")->assertOk();

        $this->assertSame(2, Commission::count());

        $agentUser = tap(User::factory()->create([
            'company_id' => $this->company->id,
            'branch_id' => $this->branch->id,
            'user_type' => 'agent',
        ]))->assignRole('agent');

        $this->agent->update(['user_id' => $agentUser->id]);

        $response = $this->actingAs($agentUser)->getJson('/api/v1/commissions')->assertOk();

        $this->assertCount(1, $response->json('data'));
        $this->assertSame($this->agent->uuid, $response->json('data.0.agent.id'));
    }

    public function test_commissions_awaiting_approval_reach_the_right_dashboard(): void
    {
        $this->rule();
        $applicant = $this->applicant();

        $this->actingAs($this->user('branch-manager'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/verify")
            ->assertOk();

        $labels = fn (string $role) => array_column(
            $this->actingAs($this->user($role))->getJson('/api/v1/dashboard')->assertOk()->json('data.attention'),
            'label',
        );

        $this->assertContains('1 commission awaiting approval', $labels('director'));
        // A cashier cannot approve commission, so it is not their work.
        $this->assertNotContains('1 commission awaiting approval', $labels('cashier'));
    }

    public function test_configuring_rules_needs_the_permission(): void
    {
        $this->actingAs($this->user('cashier'))
            ->postJson('/api/v1/commission-rules', [
                'code' => 'sneaky',
                'name' => 'Self-serve bonus',
                'trigger_event' => 'applicant_verified',
                'calculation_basis' => 'fixed',
                'currency' => 'BDT',
                'fixed_amount' => 999999,
            ])
            ->assertForbidden();
    }
}
