<?php

namespace Tests\Feature\Simulation;

use App\Models\AuditLog;
use App\Models\Branch;
use App\Models\Customer;
use App\Models\CustomerAppointment;
use App\Models\FinancialAccount;
use App\Models\Loan;
use App\Models\LoginAttempt;
use App\Models\ManagementAlert;
use App\Models\ReminderNotificationLog;
use App\Models\Task;
use App\Models\User;
use App\Services\ManagementCalendarService;
use App\Support\Authorization\PermissionName;
use App\Support\Simulation\SimulationOperationsSurfacesExpectations;
use Database\Seeders\SimulationSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class SimulationOperationsSurfacesTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        Storage::fake('local');
        config([
            'simulation.enabled' => true,
            'simulation.phase' => 'operations-surfaces',
            'simulation.anchor_date' => '2026-08-01',
            'simulation.as_of_date' => SimulationOperationsSurfacesExpectations::AS_OF_DATE,
            'simulation.user_password' => 'Simulation@2026',
        ]);
    }

    public function test_tasks_appointments_calendar_and_branch_permissions_are_deterministic(): void
    {
        $this->seed(SimulationSeeder::class);

        $overdueLoan = $this->systemWide(fn () => Loan::query()->where('purpose', SimulationOperationsSurfacesExpectations::OVERDUE_LOAN_PURPOSE)->firstOrFail());
        $dar = $this->systemWide(fn () => Branch::query()->where('code', 'SIM-DAR')->firstOrFail());
        $arusha = $this->systemWide(fn () => Branch::query()->where('code', 'SIM-ARU')->firstOrFail());
        $darManager = User::query()->where('username', 'sim.dar.manager')->firstOrFail();
        $darLoanOfficer = User::query()->where('username', 'sim.dar.loan')->firstOrFail();
        $arushaLoanOfficer = User::query()->where('username', 'sim.arusha.loan')->firstOrFail();

        $automated = $this->systemWide(fn () => Task::query()
            ->where('source_type', Loan::class)
            ->where('source_id', $overdueLoan->id)
            ->where('automation_key', 'collection:loan:'.$overdueLoan->id.':'.SimulationOperationsSurfacesExpectations::AS_OF_DATE)
            ->firstOrFail());
        $this->assertSame('pending', $automated->status);
        $this->assertSame('collection', $automated->category);
        $this->assertSame('high', $automated->priority);
        $this->assertSame((int) $dar->id, (int) $automated->branch_id);
        $this->assertSame((int) $darLoanOfficer->id, (int) $automated->assigned_to);

        $manual = $this->systemWide(fn () => Task::query()->where('automation_key', SimulationOperationsSurfacesExpectations::ARUSHA_TASK_KEY)->firstOrFail());
        $this->assertSame('completed', $manual->status);
        $this->assertSame((int) $arusha->id, (int) $manual->branch_id);
        $this->assertSame((int) $arushaLoanOfficer->id, (int) $manual->assigned_to);
        $this->assertSame((int) $arushaLoanOfficer->id, (int) $manual->completed_by);

        $appointments = $this->systemWide(fn () => CustomerAppointment::query()
            ->whereIn('title', [SimulationOperationsSurfacesExpectations::DAR_APPOINTMENT_TITLE, SimulationOperationsSurfacesExpectations::ARUSHA_APPOINTMENT_TITLE])
            ->get()->keyBy('title'));
        $this->assertCount(2, $appointments);
        $this->assertSame('scheduled', $appointments[SimulationOperationsSurfacesExpectations::DAR_APPOINTMENT_TITLE]->status);
        $this->assertSame((int) $dar->id, (int) $appointments[SimulationOperationsSurfacesExpectations::DAR_APPOINTMENT_TITLE]->branch_id);
        $this->assertSame('completed', $appointments[SimulationOperationsSurfacesExpectations::ARUSHA_APPOINTMENT_TITLE]->status);
        $this->assertSame((int) $arusha->id, (int) $appointments[SimulationOperationsSurfacesExpectations::ARUSHA_APPOINTMENT_TITLE]->branch_id);

        $calendar = app(ManagementCalendarService::class);
        $darEvents = collect($calendar->events([$dar->id], SimulationOperationsSurfacesExpectations::AS_OF_DATE, SimulationOperationsSurfacesExpectations::AS_OF_DATE));
        $arushaEvents = collect($calendar->events([$arusha->id], SimulationOperationsSurfacesExpectations::AS_OF_DATE, SimulationOperationsSurfacesExpectations::AS_OF_DATE));
        $this->assertTrue($darEvents->contains(fn (array $event): bool => $event['type'] === 'task' && $event['title'] === $automated->title));
        $this->assertTrue($darEvents->contains(fn (array $event): bool => $event['type'] === 'appointment' && $event['title'] === SimulationOperationsSurfacesExpectations::DAR_APPOINTMENT_TITLE));
        $this->assertFalse($darEvents->contains(fn (array $event): bool => $event['title'] === SimulationOperationsSurfacesExpectations::ARUSHA_TASK_TITLE));
        $this->assertFalse($darEvents->contains(fn (array $event): bool => $event['title'] === SimulationOperationsSurfacesExpectations::ARUSHA_APPOINTMENT_TITLE));
        $this->assertTrue($arushaEvents->contains(fn (array $event): bool => $event['type'] === 'task' && $event['title'] === SimulationOperationsSurfacesExpectations::ARUSHA_TASK_TITLE));
        $this->assertTrue($arushaEvents->contains(fn (array $event): bool => $event['type'] === 'appointment' && $event['title'] === SimulationOperationsSurfacesExpectations::ARUSHA_APPOINTMENT_TITLE));

        $this->assertTrue($darManager->can(PermissionName::TASKS_VIEW->value));
        $this->assertTrue($darManager->can(PermissionName::CALENDAR_VIEW->value));
        $this->assertTrue($darManager->can(PermissionName::MANAGEMENT_ALERTS_VIEW->value));
        $this->assertTrue($darLoanOfficer->can(PermissionName::TASKS_UPDATE_STATUS->value));
        $this->assertTrue($darManager->canAccessBranch((int) $dar->id));
        $this->assertFalse($darManager->canAccessBranch((int) $arusha->id));

        $this->assertFinancialControl();
    }

    public function test_reminders_alerts_audit_and_security_surfaces_use_real_persistence_paths(): void
    {
        $this->seed(SimulationSeeder::class);

        $overdueLoan = $this->systemWide(fn () => Loan::query()->where('purpose', SimulationOperationsSurfacesExpectations::OVERDUE_LOAN_PURPOSE)->firstOrFail());
        $targetLog = $this->systemWide(fn () => ReminderNotificationLog::query()
            ->where('loan_id', $overdueLoan->id)
            ->where('reminder_type', 'overdue')
            ->where('channel', 'in_system')
            ->whereDate('scheduled_for', SimulationOperationsSurfacesExpectations::AS_OF_DATE)
            ->firstOrFail());
        $this->assertSame('delivered', $targetLog->status);
        $this->assertSame(1, (int) $targetLog->attempts);
        $this->assertSame('SIM-CUS-DAR-003', $targetLog->recipient);

        $customer = $this->systemWide(fn () => Customer::query()->findOrFail($overdueLoan->customer_id));
        $notification = $customer->notifications->first(fn ($item): bool => (int) ($item->data['notification_log_id'] ?? 0) === (int) $targetLog->id);
        $this->assertNotNull($notification);
        $this->assertSame('loan_payment_reminder', $notification->data['event']);

        $alert = $this->systemWide(fn () => ManagementAlert::query()
            ->where('alert_key', 'overdue_threshold:'.$overdueLoan->id.':'.SimulationOperationsSurfacesExpectations::OVERDUE_DAYS_THRESHOLD)
            ->firstOrFail());
        $this->assertSame('overdue_threshold', $alert->type);
        $this->assertSame('warning', $alert->severity);
        $this->assertSame('open', $alert->status);
        $this->assertSame((int) $overdueLoan->branch_id, (int) $alert->branch_id);

        $success = LoginAttempt::query()->where('login_identifier', 'sim.dar.loan')->where('status', 'success')
            ->whereDate('attempted_at', SimulationOperationsSurfacesExpectations::AS_OF_DATE)->get();
        $failure = LoginAttempt::query()->where('login_identifier', SimulationOperationsSurfacesExpectations::FAILED_LOGIN_IDENTIFIER)->where('status', 'failed')
            ->whereDate('attempted_at', SimulationOperationsSurfacesExpectations::AS_OF_DATE)->get();
        $this->assertCount(1, $success);
        $this->assertCount(1, $failure);
        $this->assertSame('10.10.0.11', $success->first()->ip_address);
        $this->assertSame('10.10.0.99', $failure->first()->ip_address);

        $manualTask = $this->systemWide(fn () => Task::query()->where('automation_key', SimulationOperationsSurfacesExpectations::ARUSHA_TASK_KEY)->firstOrFail());
        $appointments = $this->systemWide(fn () => CustomerAppointment::query()
            ->whereIn('title', [SimulationOperationsSurfacesExpectations::DAR_APPOINTMENT_TITLE, SimulationOperationsSurfacesExpectations::ARUSHA_APPOINTMENT_TITLE])->get());
        $auditableIds = $appointments->pluck('id')->all();
        $taskAudits = AuditLog::query()->where('auditable_type', Task::class)->where('auditable_id', $manualTask->id)
            ->whereIn('action', ['task.created', 'task.status_changed'])->count();
        $appointmentAudits = AuditLog::query()->where('auditable_type', CustomerAppointment::class)->whereIn('auditable_id', $auditableIds)
            ->whereIn('action', ['appointment.created', 'appointment.status_changed'])->count();
        $this->assertSame(2, $taskAudits);
        $this->assertSame(3, $appointmentAudits);

        $this->assertFinancialControl();
    }

    public function test_operations_surfaces_simulation_is_idempotent_and_non_financial(): void
    {
        $this->seed(SimulationSeeder::class);
        $before = $this->snapshot();

        $this->seed(SimulationSeeder::class);
        $after = $this->snapshot();

        $this->assertSame($before, $after);
        $this->assertSame(SimulationOperationsSurfacesExpectations::FINANCIAL_CONTROL, $after['account_total']);
    }

    /** @return array<string, int|string> */
    private function snapshot(): array
    {
        $overdueLoan = $this->systemWide(fn () => Loan::query()->where('purpose', SimulationOperationsSurfacesExpectations::OVERDUE_LOAN_PURPOSE)->firstOrFail());
        $manualTask = $this->systemWide(fn () => Task::query()->where('automation_key', SimulationOperationsSurfacesExpectations::ARUSHA_TASK_KEY)->firstOrFail());
        $appointments = $this->systemWide(fn () => CustomerAppointment::query()
            ->whereIn('title', [SimulationOperationsSurfacesExpectations::DAR_APPOINTMENT_TITLE, SimulationOperationsSurfacesExpectations::ARUSHA_APPOINTMENT_TITLE])->get());
        $appointmentIds = $appointments->pluck('id')->all();
        $targetLog = $this->systemWide(fn () => ReminderNotificationLog::query()
            ->where('loan_id', $overdueLoan->id)->where('reminder_type', 'overdue')->where('channel', 'in_system')
            ->whereDate('scheduled_for', SimulationOperationsSurfacesExpectations::AS_OF_DATE)->firstOrFail());
        $customer = $this->systemWide(fn () => Customer::query()->findOrFail($overdueLoan->customer_id));

        return [
            'automated_task' => $this->systemWide(fn () => Task::query()->where('automation_key', 'collection:loan:'.$overdueLoan->id.':'.SimulationOperationsSurfacesExpectations::AS_OF_DATE)->count()),
            'manual_task' => $this->systemWide(fn () => Task::query()->where('automation_key', SimulationOperationsSurfacesExpectations::ARUSHA_TASK_KEY)->count()),
            'appointments' => $appointments->count(),
            'target_reminder' => $this->systemWide(fn () => ReminderNotificationLog::query()->whereKey($targetLog->id)->count()),
            'target_notification' => $customer->notifications->filter(fn ($item): bool => (int) ($item->data['notification_log_id'] ?? 0) === (int) $targetLog->id)->count(),
            'target_alert' => $this->systemWide(fn () => ManagementAlert::query()->where('alert_key', 'overdue_threshold:'.$overdueLoan->id.':'.SimulationOperationsSurfacesExpectations::OVERDUE_DAYS_THRESHOLD)->count()),
            'login_attempts' => LoginAttempt::query()->whereDate('attempted_at', SimulationOperationsSurfacesExpectations::AS_OF_DATE)
                ->where(function ($query): void {
                    $query->where(function ($nested): void { $nested->where('login_identifier', 'sim.dar.loan')->where('status', 'success'); })
                        ->orWhere(function ($nested): void { $nested->where('login_identifier', SimulationOperationsSurfacesExpectations::FAILED_LOGIN_IDENTIFIER)->where('status', 'failed'); });
                })->count(),
            'target_audits' => AuditLog::query()->where(function ($query) use ($manualTask, $appointmentIds): void {
                $query->where(function ($nested) use ($manualTask): void {
                    $nested->where('auditable_type', Task::class)->where('auditable_id', $manualTask->id)
                        ->whereIn('action', ['task.created', 'task.status_changed']);
                })->orWhere(function ($nested) use ($appointmentIds): void {
                    $nested->where('auditable_type', CustomerAppointment::class)->whereIn('auditable_id', $appointmentIds)
                        ->whereIn('action', ['appointment.created', 'appointment.status_changed']);
                });
            })->count(),
            'account_total' => $this->money($this->systemWide(fn () => FinancialAccount::query()->where('name', 'like', 'SIM %')->sum('current_balance'))),
        ];
    }

    private function assertFinancialControl(): void
    {
        $total = $this->systemWide(fn () => FinancialAccount::query()->where('name', 'like', 'SIM %')->sum('current_balance'));
        $this->assertSame(SimulationOperationsSurfacesExpectations::FINANCIAL_CONTROL, $this->money($total));
    }

    private function money(mixed $value): string
    {
        return number_format((float) $value, 2, '.', '');
    }
}
