<?php

namespace App\Tests\Billing;

use App\Auth\Entity\User;
use App\Billing\Entity\Claim;
use App\Billing\Entity\ClaimItem;
use App\Billing\Entity\Invoice;
use App\Billing\Entity\InvoiceItem;
use App\Billing\ValueObject\ShareBreakdown;
use App\Doctor\Entity\Doctor;
use App\Insurance\Entity\Insurance;
use App\Insurance\Enum\InsuranceType;
use App\Patient\Entity\PatientRecord;
use App\Tests\ApiTestCase;

/**
 * GET /api/v1/billing/claims/by-patient — the claims dashboard's first level.
 *
 * A claim reaches its patient only through claim_item → invoice_item → invoice →
 * patient_record, and one invoice can carry both a base and a supplementary claim.
 * These tests pin the aggregation against double-counting the service amount.
 */
class ClaimsByPatientTest extends ApiTestCase
{
    private User $owner;
    private Doctor $doctor;
    private PatientRecord $record;

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

        $this->owner  = $this->createUser(['ROLE_DOCTOR']);
        $this->doctor = new Doctor($this->owner, 'دکتر تست');
        $this->em->persist($this->doctor);
        $this->em->flush();

        $patient      = $this->createUser(['ROLE_USER']);
        $this->record = new PatientRecord('doctor', $this->doctor->getId(), $patient, 'doctor', $this->doctor->getId());
        $this->em->persist($this->record);
        $this->em->flush();
    }

    /**
     * Invoice of $total split into insurance/patient shares, with its claim(s).
     *
     * @param list<int> $insuranceIds one per claim; defaults to synthetic ids when the
     *                                test does not care which insurance it is
     */
    private function invoiceWithClaims(int $total, int $baseShare, int $suppShare, array $statuses = ['pending'], array $insuranceIds = []): Invoice
    {
        $patient = $total - $baseShare - $suppShare;

        $invoice = new Invoice('doctor', $this->doctor->getId());
        $invoice->setPatientRecordId((int) $this->record->getId());
        $item    = new InvoiceItem($invoice, 'جراحی', $total, 1, new ShareBreakdown($total, $baseShare, $suppShare, $patient));
        $invoice->addItem($item);
        $invoice->recalculateTotals();
        $this->em->persist($invoice);
        $this->em->persist($item);
        $this->em->flush();

        foreach ($statuses as $i => $status) {
            $kind  = $i === 0 ? Claim::KIND_BASE : Claim::KIND_SUPPLEMENTARY;
            $share = $i === 0 ? $baseShare : $suppShare;

            $claim     = new Claim('doctor', $this->doctor->getId(), $insuranceIds[$i] ?? (1 + $i), $kind);
            $claimItem = new ClaimItem($claim, (int) $item->getId(), $share);
            $claim->addItem($claimItem);
            if ($status !== Claim::STATUS_PENDING) {
                $claim->submit();
            }
            if ($status === Claim::STATUS_PAID) {
                $claim->approve($share);
                $claim->pay($share);
            }
            $this->em->persist($claim);
            $this->em->persist($claimItem);
        }
        $this->em->flush();

        return $invoice;
    }

    public function testAggregatesOneRowPerPatientWithConsistentShares(): void
    {
        $this->invoiceWithClaims(10_000_000, 7_000_000, 0);

        $res = $this->authJson('GET', '/api/v1/billing/claims/by-patient', $this->owner);
        self::assertSame(200, $this->responseCode());
        self::assertCount(1, $res['data']);

        $row = $res['data'][0];
        self::assertSame(1, $row['claims_count']);
        self::assertSame(10_000_000, $row['total_services_rials']);
        self::assertSame(7_000_000, $row['total_insurance_rials']);
        self::assertSame(3_000_000, $row['total_patient_rials']);
        self::assertSame('pending', $row['overall_status']);
    }

    public function testServiceTotalIsNotDoubleCountedWhenAnInvoiceHasTwoClaims(): void
    {
        // پایه و مکمل روی یک صورتحساب: مبلغ خدمات باید یک‌بار شمرده شود.
        $this->invoiceWithClaims(10_000_000, 6_000_000, 2_000_000, ['pending', 'pending']);

        $res = $this->authJson('GET', '/api/v1/billing/claims/by-patient', $this->owner);
        $row = $res['data'][0];

        self::assertSame(2, $row['claims_count']);
        self::assertSame(10_000_000, $row['total_services_rials']);
        self::assertSame(8_000_000, $row['total_insurance_rials']);
        self::assertSame(
            $row['total_services_rials'],
            $row['total_insurance_rials'] + $row['total_patient_rials'],
        );
    }

    public function testOverallStatusIsMixedWhenClaimsDisagree(): void
    {
        $this->invoiceWithClaims(10_000_000, 6_000_000, 2_000_000, [Claim::STATUS_PAID, Claim::STATUS_PENDING]);

        $res = $this->authJson('GET', '/api/v1/billing/claims/by-patient', $this->owner);

        self::assertSame('mixed', $res['data'][0]['overall_status']);
    }

    public function testEachRowCarriesTheInsurancesItsClaimsBelongTo(): void
    {
        $iran = new Insurance('بیمه ایران تست', InsuranceType::Basic);
        $asia = new Insurance('بیمه آسیا تست', InsuranceType::Supplementary);
        $this->em->persist($iran);
        $this->em->persist($asia);
        $this->em->flush();

        $this->invoiceWithClaims(10_000_000, 6_000_000, 2_000_000, ['pending', 'pending'], [
            (int) $iran->getId(), (int) $asia->getId(),
        ]);

        $res = $this->authJson('GET', '/api/v1/billing/claims/by-patient', $this->owner);
        $row = $res['data'][0];

        self::assertCount(2, $row['insurances']);
        $names = array_column($row['insurances'], 'insurance_name');
        self::assertContains('بیمه ایران تست', $names);
        self::assertContains('بیمه آسیا تست', $names);
        self::assertSame(
            ['base', 'supplementary'],
            array_values(array_unique(array_column($row['insurances'], 'kind'))),
        );
    }

    public function testKindFilterKeepsOnlyThatKindOfClaim(): void
    {
        $this->invoiceWithClaims(10_000_000, 6_000_000, 2_000_000, ['pending', 'pending']);

        $supp = $this->authJson('GET', '/api/v1/billing/claims/by-patient?kind=supplementary', $this->owner);
        self::assertSame(1, $supp['data'][0]['claims_count']);
        self::assertSame(2_000_000, $supp['data'][0]['total_insurance_rials']);

        $base = $this->authJson('GET', '/api/v1/billing/claims/by-patient?kind=base', $this->owner);
        self::assertSame(6_000_000, $base['data'][0]['total_insurance_rials']);
    }

    public function testAnUnknownKindIsRejected(): void
    {
        $body = $this->authJson('GET', '/api/v1/billing/claims/by-patient?kind=bogus', $this->owner);

        self::assertSame(422, $this->responseCode());
        self::assertSame('kind', $body['errors'][0]['field']);
    }

    public function testSearchAlsoMatchesTheInsuranceName(): void
    {
        $asia = new Insurance('بیمه آسیا جستجو', InsuranceType::Supplementary);
        $this->em->persist($asia);
        $this->em->flush();

        $this->invoiceWithClaims(10_000_000, 0, 4_000_000, ['pending'], [(int) $asia->getId()]);

        $hit = $this->authJson('GET', '/api/v1/billing/claims/by-patient?search=' . urlencode('آسیا'), $this->owner);
        self::assertSame(1, $hit['meta']['totalRecords']);

        $miss = $this->authJson('GET', '/api/v1/billing/claims/by-patient?search=' . urlencode('بیمه ناموجود'), $this->owner);
        self::assertSame(0, $miss['meta']['totalRecords']);
        self::assertSame([], $miss['data']);
    }

    public function testStatusFilterNarrowsTheAggregation(): void
    {
        $this->invoiceWithClaims(10_000_000, 7_000_000, 0);

        $res = $this->authJson('GET', '/api/v1/billing/claims/by-patient?status=paid', $this->owner);

        self::assertSame(0, $res['meta']['totalRecords']);
    }

    public function testDetailListsClaimsWithCoverageAndTimeline(): void
    {
        $this->invoiceWithClaims(10_000_000, 7_000_000, 0);

        $res = $this->authJson('GET', '/api/v1/billing/claims/by-patient/' . $this->record->getUuid(), $this->owner);
        self::assertSame(200, $this->responseCode());

        $claim = $res['data']['claims'][0];
        // JSON یک float گِرد را به int تبدیل می‌کند؛ مقایسه‌ی نوع‌محور اینجا معنا ندارد.
        self::assertEquals(70, $claim['coverage_percent']);
        self::assertSame(7_000_000, $claim['insurance_share_rials']);
        self::assertSame(3_000_000, $claim['patient_share_rials']);
        self::assertSame(['submitted'], $claim['allowed_transitions']);
    }

    public function testDetailRefusesARecordOfAnotherTenant(): void
    {
        $otherOwner  = $this->createUser(['ROLE_DOCTOR']);
        $otherDoctor = new Doctor($otherOwner, 'دکتر دیگر');
        $this->em->persist($otherDoctor);
        $this->em->flush();

        $this->authJson('GET', '/api/v1/billing/claims/by-patient/' . $this->record->getUuid(), $otherOwner);

        self::assertSame(404, $this->responseCode());
    }

    public function testSubmitStoresTheTrackingNumberAndLogsTheTransition(): void
    {
        $this->invoiceWithClaims(10_000_000, 7_000_000, 0);
        $detail = $this->authJson('GET', '/api/v1/billing/claims/by-patient/' . $this->record->getUuid(), $this->owner);
        $uuid   = $detail['data']['claims'][0]['uuid'];

        $this->authJson('POST', '/api/v1/billing/claims/' . $uuid . '/submit', $this->owner, [
            'tracking_number' => 'TM-1',
        ]);
        self::assertSame(200, $this->responseCode());

        $after = $this->authJson('GET', '/api/v1/billing/claims/by-patient/' . $this->record->getUuid(), $this->owner);
        $claim = $after['data']['claims'][0];

        self::assertSame('TM-1', $claim['tracking_number']);
        self::assertSame('submitted', $claim['status']);
        self::assertSame('submitted', end($claim['logs'])['to_status']);
    }

    public function testRejectWithoutAReasonIsRefused(): void
    {
        $this->invoiceWithClaims(10_000_000, 7_000_000, 0, [Claim::STATUS_SUBMITTED]);
        $detail = $this->authJson('GET', '/api/v1/billing/claims/by-patient/' . $this->record->getUuid(), $this->owner);
        $uuid   = $detail['data']['claims'][0]['uuid'];

        $this->authJson('POST', '/api/v1/billing/claims/' . $uuid . '/reject', $this->owner, []);

        self::assertSame(422, $this->responseCode());
    }

    /**
     * این داشبورد با SQL خام ساخته می‌شود، پس TenantFilter آن را نمی‌بیند و تنها
     * محافظش شرط دستیِ `c.entity_type/:entity_id` در ClaimRepository است. اگر آن
     * WHERE روزی برداشته شود، هیچ چیز جز این تست خبر نمی‌دهد.
     */
    public function testAnotherTenantsClaimsNeverAppearInTheDashboard(): void
    {
        $this->invoiceWithClaims(10_000_000, 7_000_000, 0);

        $strangerUser   = $this->createUser(['ROLE_DOCTOR']);
        $strangerDoctor = new Doctor($strangerUser, 'دکتر بیگانه');
        $this->em->persist($strangerDoctor);
        $this->em->flush();

        $res = $this->authJson('GET', '/api/v1/billing/claims/by-patient', $strangerUser);

        self::assertSame(200, $this->responseCode());
        self::assertSame([], $res['data'], 'مطالبات پزشک دیگر نباید دیده شود');
    }
}
