<?php

namespace Tests\Feature\CreditScore;

use App\Models\Branch;
use App\Models\Business;
use App\Services\CreditScore\CreditScoreCalibrationReportService;
use App\Services\CreditScore\CreditScorePortfolioReportService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Console\Command;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;

class CreditScoreFinalHardeningTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed(DatabaseSeeder::class);
    }

    public function test_v1_configuration_is_frozen_and_internally_consistent(): void
    {
        $this->assertSame('v1', config('credit_score.version'));
        $this->assertSame(300, config('credit_score.score.minimum'));
        $this->assertSame(850, config('credit_score.score.maximum'));
        $this->assertSame([
            'repayment_behaviour' => 40,
            'loan_history' => 20,
            'affordability' => 20,
            'collateral' => 10,
            'profile_completeness' => 5,
            'conduct_flags' => 5,
        ], config('credit_score.weights'));

        $bands = collect(config('credit_score.bands'))
            ->sortBy('minimum')
            ->values();
        $expectedMinimum = 300;

        foreach ($bands as $band) {
            $this->assertSame($expectedMinimum, (int) $band['minimum']);
            $this->assertGreaterThanOrEqual((int) $band['minimum'], (int) $band['maximum']);
            $this->assertNotEmpty($band['risk_level']);
            $expectedMinimum = (int) $band['maximum'] + 1;
        }

        $this->assertSame(851, $expectedMinimum);
        $this->assertSame(100, array_sum(config('credit_score.weights')));
    }

    public function test_credit_score_production_gate_passes_on_migrated_seeded_schema(): void
    {
        $this->artisan('credit-score:production-check')
            ->assertExitCode(Command::SUCCESS);
    }

    public function test_credit_score_production_gate_rejects_silent_v1_weight_drift(): void
    {
        config()->set('credit_score.weights.repayment_behaviour', 39);

        $this->artisan('credit-score:production-check')
            ->assertExitCode(Command::FAILURE);
    }

    public function test_credit_score_schema_has_reporting_indexes_and_application_snapshot_column(): void
    {
        $this->assertTrue(Schema::hasColumn('loans', 'application_credit_score_history_id'));
        $this->assertTrue($this->hasIndexColumns('customer_risk_profiles', ['score_band', 'calculated_at']));
        $this->assertTrue($this->hasIndexColumns('customer_risk_profiles', ['score_version', 'calculated_at']));
        $this->assertTrue($this->hasIndexColumns('customer_credit_score_histories', ['customer_id', 'calculated_at']));
        $this->assertTrue($this->hasIndexColumns('customer_credit_score_histories', ['branch_id', 'calculated_at']));
        $this->assertTrue($this->hasIndexColumns('customer_credit_score_histories', ['score_band', 'calculated_at']));
        $this->assertTrue($this->hasIndexColumns('customer_credit_score_histories', ['score_version', 'calculated_at']));
    }

    public function test_credit_score_services_do_not_access_prohibited_customer_traits(): void
    {
        $paths = collect(File::allFiles(app_path('Services/CreditScore')))
            ->map(fn ($file) => $file->getPathname())
            ->push(app_path('Services/CustomerRiskService.php'));
        $prohibited = [
            'gender',
            'marital_status',
            'date_of_birth',
            'ethnicity',
            'religion',
            'region',
            'district',
            'ward',
            'nin',
            'national_id',
        ];
        $field = implode('|', array_map(fn (string $value) => preg_quote($value, '/'), $prohibited));
        $patterns = [
            '/->(?:'.$field.')\b/i',
            '/[\'\"](?:'.$field.')[\'\"]\s*(?:=>|,|\))/i',
            '/\b(?:where|orWhere|whereNotNull|whereNull)\s*\(\s*[\'\"](?:'.$field.')[\'\"]/i',
        ];

        foreach ($paths as $path) {
            $source = File::get($path);
            foreach ($patterns as $pattern) {
                $this->assertDoesNotMatchRegularExpression(
                    $pattern,
                    $source,
                    'Prohibited customer trait referenced by credit-score service: '.str($path)->after(base_path().DIRECTORY_SEPARATOR),
                );
            }
        }
    }

    public function test_credit_score_reports_keep_bounded_query_counts(): void
    {
        $business = Business::query()->firstOrFail();
        $branch = Branch::factory()->create(['business_id' => $business->id]);

        DB::flushQueryLog();
        DB::enableQueryLog();
        app(CreditScorePortfolioReportService::class)->generate([$branch->id]);
        $portfolioQueries = count(DB::getQueryLog());
        DB::disableQueryLog();

        DB::flushQueryLog();
        DB::enableQueryLog();
        app(CreditScoreCalibrationReportService::class)->generate([$branch->id], 'v1');
        $calibrationQueries = count(DB::getQueryLog());
        DB::disableQueryLog();

        $this->assertLessThanOrEqual(8, $portfolioQueries, 'Credit Score Portfolio report exceeded its bounded query budget.');
        $this->assertLessThanOrEqual(4, $calibrationQueries, 'Credit Score Calibration report exceeded its bounded query budget.');
    }

    private function hasIndexColumns(string $table, array $columns): bool
    {
        return collect(Schema::getIndexes($table))
            ->contains(fn (array $index) => array_values($index['columns'] ?? []) === $columns);
    }
}
