<?php

namespace App\Tests\Billing;

use App\Auth\Entity\User;
use App\Billing\Entity\Invoice;
use App\Billing\Entity\InvoiceItem;
use App\Billing\ValueObject\ShareBreakdown;
use App\Doctor\Entity\Doctor;
use App\Patient\Entity\PatientRecord;
use App\Patient\Entity\PatientSession;
use App\Patient\Entity\SessionPayment;
use App\Tests\ApiTestCase;
use App\UserProfile\Entity\UserProfile;

/**
 * GET /api/v1/my/billing/payments — flat, tenant-scoped list of recorded
 * invoices, and GET .../patients/{uuid}/invoices — one patient's invoices.
 */
class PatientPaymentsTest extends ApiTestCase
{
    /** @return array{0: User, 1: Doctor} owner user + their doctor profile */
    private function doctor(): array
    {
        $owner  = $this->createUser(['ROLE_DOCTOR']);
        $doctor = new Doctor($owner, 'دکتر تست');
        $this->em->persist($doctor);
        $this->em->flush();

        return [$owner, $doctor];
    }

    // national_code is UNIQUE and db_test is never reset, so randomise it per
    // patient (like mobile) to avoid cross-run collisions; read it back from the
    // record's user when a test needs to filter by it.
    private function patientRecord(Doctor $doctor, string $realName): PatientRecord
    {
        $patient = $this->createUser(['ROLE_USER']);
        $this->setField($patient, 'realName', $realName);
        $this->setField($patient, 'nationalCode', str_pad((string) random_int(0, 9_999_999_999), 10, '0', STR_PAD_LEFT));

        $record = new PatientRecord('doctor', $doctor->getId(), $patient, 'doctor', $doctor->getId());
        $this->em->persist($record);
        $this->em->flush();

        return $record;
    }

    private function nationalCodeOf(PatientRecord $record): string
    {
        return $record->getUser()->getNationalCode();
    }

    /**
     * `$status` چرخه‌ی حیات صورتحساب است (draft/finalized/void). وضعیت پرداختِ
     * نمایش‌داده‌شده از `$paidRials` مشتق می‌شود — پرداخت واقعی روی مراجعه.
     */
    private function invoice(
        Doctor $doctor,
        PatientRecord $record,
        string $status,
        int $patientRials,
        ?int $issuedAt = null,
        ?string $itemTitle = null,
        int $totalRials = 0,
        int $paidRials = 0,
    ): Invoice {
        $invoice = new Invoice('doctor', $doctor->getId());
        $invoice->setPatientRecordId($record->getId());
        if ($paidRials > 0) {
            $invoice->setPatientSessionId($this->paidSession($record, $paidRials)->getId());
        }
        if ($itemTitle !== null) {
            // Item added only for its title (service_title); totals set below by hand.
            $invoice->addItem(new InvoiceItem($invoice, $itemTitle, $totalRials, 1, new ShareBreakdown($totalRials, 0, 0, $patientRials), null));
        }
        $this->setField($invoice, 'status', $status);
        $this->setField($invoice, 'patientRials', $patientRials);
        $this->setField($invoice, 'totalRials', $totalRials);
        if ($issuedAt !== null) {
            $this->setField($invoice, 'issuedAt', $issuedAt);
        }
        $this->em->persist($invoice);
        $this->em->flush();

        return $invoice;
    }

    /** مراجعه‌ای با یک پرداخت نقدی ثبت‌شده — منبع واقعیِ «چقدر وصول شده». */
    private function paidSession(PatientRecord $record, int $paidRials): PatientSession
    {
        $session = new PatientSession($record);
        $this->em->persist($session);
        $this->em->flush();

        $payment = new SessionPayment($session, 'cash', $paidRials, time());
        $this->em->persist($payment);
        $this->em->flush();

        return $session;
    }

    private function setField(object $obj, string $prop, mixed $value): void
    {
        $ref = new \ReflectionProperty($obj, $prop);
        $ref->setAccessible(true);
        $ref->setValue($obj, $value);
    }

    public function testListsFlatInvoicesNewestFirst(): void
    {
        [$owner, $doctor] = $this->doctor();

        $a = $this->patientRecord($doctor, 'دنیا خلیلی');
        $this->invoice($doctor, $a, Invoice::STATUS_FINALIZED, 100000, 2000, null, 0, 100000); // fully paid
        $this->invoice($doctor, $a, Invoice::STATUS_FINALIZED, 50000, 1000);                   // nothing paid
        $this->invoice($doctor, $a, Invoice::STATUS_DRAFT, 999999, 3000); // excluded
        $this->invoice($doctor, $a, Invoice::STATUS_VOID, 999999, 3000);  // excluded

        $res = $this->authJson('GET', '/api/v1/my/billing/payments', $owner);
        self::assertSame(200, $this->responseCode());
        self::assertSame(2, $res['meta']['totalRecords']); // draft/void excluded

        // newest (issued_at DESC) first
        self::assertSame('دنیا خلیلی', $res['data'][0]['patient_name']);
        self::assertSame($this->nationalCodeOf($a), $res['data'][0]['national_code']);
        self::assertSame(100000, $res['data'][0]['amount_rials']);
        self::assertSame('paid', $res['data'][0]['status']);
        self::assertArrayHasKey('invoice_uuid', $res['data'][0]);
        self::assertSame($a->getUuid(), $res['data'][0]['patient_uuid']);

        self::assertSame(50000, $res['data'][1]['amount_rials']);
        self::assertSame('unsettled', $res['data'][1]['status']);
    }

    public function testFiltersByNationalCodeAndStatus(): void
    {
        [$owner, $doctor] = $this->doctor();
        $paid    = $this->patientRecord($doctor, 'بیمار پرداخت');
        $unpaid  = $this->patientRecord($doctor, 'بیمار بدهکار');
        $partial = $this->patientRecord($doctor, 'بیمار نیمه‌پرداخت');
        $this->invoice($doctor, $paid, Invoice::STATUS_FINALIZED, 100000, null, null, 0, 100000);
        $this->invoice($doctor, $unpaid, Invoice::STATUS_FINALIZED, 100000);
        $this->invoice($doctor, $partial, Invoice::STATUS_FINALIZED, 100000, null, null, 0, 40000);

        $byCode = $this->authJson('GET', '/api/v1/my/billing/payments?national_code=' . $this->nationalCodeOf($paid), $owner);
        self::assertSame(1, $byCode['meta']['totalRecords']);
        self::assertSame($paid->getUuid(), $byCode['data'][0]['patient_uuid']);

        $onlyPaid = $this->authJson('GET', '/api/v1/my/billing/payments?status=paid', $owner);
        self::assertSame(1, $onlyPaid['meta']['totalRecords']);
        self::assertSame('paid', $onlyPaid['data'][0]['status']);

        $onlyUnsettled = $this->authJson('GET', '/api/v1/my/billing/payments?status=unsettled', $owner);
        self::assertSame(1, $onlyUnsettled['meta']['totalRecords']);
        self::assertSame($unpaid->getUuid(), $onlyUnsettled['data'][0]['patient_uuid']);

        $onlyPartial = $this->authJson('GET', '/api/v1/my/billing/payments?status=partial', $owner);
        self::assertSame(1, $onlyPartial['meta']['totalRecords']);
        self::assertSame($partial->getUuid(), $onlyPartial['data'][0]['patient_uuid']);
        self::assertSame('partial', $onlyPartial['data'][0]['status']);
        self::assertSame(40000, $onlyPartial['data'][0]['paid_rials']);
    }

    /** بیمارِ ساخته‌شده در نوبت‌دهی کد ملی را روی پروفایل دارد نه روی users. */
    private function patientRecordWithProfileCode(Doctor $doctor, string $realName, string $nationalCode): PatientRecord
    {
        $patient = $this->createUser(['ROLE_USER']);
        $this->setField($patient, 'realName', $realName);
        // users.national_code خالی؛ کد ملی فقط روی پروفایل.
        $profile = new UserProfile($patient);
        $profile->setNationalCode($nationalCode);
        $this->em->persist($profile);

        $record = new PatientRecord('doctor', $doctor->getId(), $patient, 'doctor', $doctor->getId());
        $this->em->persist($record);
        $this->em->flush();

        return $record;
    }

    public function testNationalCodeFromProfileSurfacesInListAndDetail(): void
    {
        [$owner, $doctor] = $this->doctor();
        $nc     = str_pad((string) random_int(0, 9_999_999_999), 10, '0', STR_PAD_LEFT);
        $record = $this->patientRecordWithProfileCode($doctor, 'بیمار پروفایلی', $nc);
        $this->invoice($doctor, $record, Invoice::STATUS_FINALIZED, 100000, 1000, null, 0, 100000);

        // لیست
        $list = $this->authJson('GET', '/api/v1/my/billing/payments', $owner);
        self::assertSame($nc, $list['data'][0]['national_code']);

        // فیلتر با کد ملیِ پروفایل
        $filtered = $this->authJson('GET', '/api/v1/my/billing/payments?national_code=' . $nc, $owner);
        self::assertSame(1, $filtered['meta']['totalRecords']);

        // هدرِ صفحهٔ جزئیات
        $detail = $this->authJson('GET', '/api/v1/my/billing/patients/' . $record->getUuid() . '/invoices', $owner);
        self::assertSame($nc, $detail['data']['patient']['national_code']);
    }

    public function testEmptyWhenNoInvoices(): void
    {
        [$owner] = $this->doctor();
        $res = $this->authJson('GET', '/api/v1/my/billing/payments', $owner);
        self::assertSame(200, $this->responseCode());
        self::assertSame(0, $res['meta']['totalRecords']);
        self::assertCount(0, $res['data']);
    }

    public function testForbiddenWithoutProfile(): void
    {
        $orphan = $this->createUser(['ROLE_DOCTOR']); // ROLE_DOCTOR but no Doctor row
        $this->authJson('GET', '/api/v1/my/billing/payments', $orphan);
        self::assertSame(403, $this->responseCode());
    }

    // ── Node 2: a patient's recorded invoices ────────────────────────────────

    public function testListsPatientInvoicesWithHeaderAndDerivedStatus(): void
    {
        [$owner, $doctor] = $this->doctor();
        $record = $this->patientRecord($doctor, 'دنیا خلیلی');

        $this->invoice($doctor, $record, Invoice::STATUS_FINALIZED, 235000, 1000, 'روکش دندان', 235000, 235000);
        $this->invoice($doctor, $record, Invoice::STATUS_FINALIZED, 600000, 2000, 'طرح لبخند', 600000);
        $this->invoice($doctor, $record, Invoice::STATUS_DRAFT, 111, 3000, 'پیش‌نویس', 111); // excluded

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

        // header
        self::assertSame('دنیا خلیلی', $res['data']['patient']['name']);
        self::assertSame($this->nationalCodeOf($record), $res['data']['patient']['national_code']);

        // invoices — draft excluded, newest (issued_at DESC) first
        self::assertSame(2, $res['data']['meta']['totalRecords']);
        $rows = $res['data']['data'];
        self::assertCount(2, $rows);
        self::assertSame('طرح لبخند', $rows[0]['service_title']);
        self::assertSame('unsettled', $rows[0]['status']);
        self::assertSame(600000, $rows[0]['total_rials']);
        self::assertSame('روکش دندان', $rows[1]['service_title']);
        self::assertSame('paid', $rows[1]['status']);
    }

    public function testPatientInvoiceRowsCarryTheirPaymentMethods(): void
    {
        [$owner, $doctor] = $this->doctor();
        $record  = $this->patientRecord($doctor, 'دنیا خلیلی');
        $session = $this->paidSession($record, 60000);

        $extra = new SessionPayment($session, 'pos', 40000, time() + 60);
        $this->em->persist($extra);
        $this->em->flush();

        $invoice = $this->invoice($doctor, $record, Invoice::STATUS_FINALIZED, 100000, 1000, 'روکش دندان', 100000);
        $invoice->setPatientSessionId($session->getId());
        $this->em->flush();

        $res  = $this->authJson('GET', '/api/v1/my/billing/patients/' . $record->getUuid() . '/invoices', $owner);
        $row  = $res['data']['data'][0];

        self::assertSame('paid', $row['status']);
        self::assertSame(100000, $row['paid_rials']);
        self::assertCount(2, $row['payments']);
        // oldest payment first
        self::assertSame('cash', $row['payments'][0]['method']);
        self::assertSame(60000, $row['payments'][0]['amount_rials']);
        self::assertSame('pos', $row['payments'][1]['method']);
        self::assertSame(40000, $row['payments'][1]['amount_rials']);
    }

    public function testPatientInvoiceWithoutSessionHasNoPayments(): void
    {
        [$owner, $doctor] = $this->doctor();
        $record = $this->patientRecord($doctor, 'بدون مراجعه');
        $this->invoice($doctor, $record, Invoice::STATUS_FINALIZED, 100000, 1000, 'ویزیت', 100000);

        $res = $this->authJson('GET', '/api/v1/my/billing/patients/' . $record->getUuid() . '/invoices', $owner);
        $row = $res['data']['data'][0];

        self::assertSame([], $row['payments']);
        self::assertSame(0, $row['paid_rials']);
        self::assertSame('unsettled', $row['status']);
    }

    public function testPatientInvoicesNotFoundForOtherTenant(): void
    {
        [, $doctor] = $this->doctor();
        $record = $this->patientRecord($doctor, 'بیمار');

        [$other] = $this->doctor();
        $this->authJson('GET', '/api/v1/my/billing/patients/' . $record->getUuid() . '/invoices', $other);
        self::assertSame(404, $this->responseCode());
    }

    public function testPatientInvoicesEmpty(): void
    {
        [$owner, $doctor] = $this->doctor();
        $record = $this->patientRecord($doctor, 'بدون فاکتور');

        $res = $this->authJson('GET', '/api/v1/my/billing/patients/' . $record->getUuid() . '/invoices', $owner);
        self::assertSame(200, $this->responseCode());
        self::assertSame(0, $res['data']['meta']['totalRecords']);
        self::assertCount(0, $res['data']['data']);
        self::assertSame('بدون فاکتور', $res['data']['patient']['name']);
    }
}
