<?php

declare(strict_types=1);

namespace Tests\Feature\Test;

use App\Models\Test\TestDefSiteModel;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;

class TestCreateVariantsTest extends CIUnitTestCase
{
    use FeatureTestTrait;

    private const SITE_ID = 1;

    protected string $endpoint = 'api/test';

    private TestDefSiteModel $testModel;

    protected function setUp(): void
    {
        parent::setUp();
        $this->testModel = new TestDefSiteModel();
    }

    public function testCreateTechnicalWithoutReferenceOrTestMap(): void
    {
        foreach (['TEST', 'PARAM'] as $type) {
            $this->assertTechnicalCreated($type);
        }
    }

    public function testCreateTechnicalCanAcceptNullSiteAndNumericFields(): void
    {
        $payload = $this->buildTechnicalPayload('TEST', [
            'ResultType' => 'NMRIC',
            'RefType' => 'RANGE',
            'Decimal' => null,
            'Factor' => 2.5,
            'ReqQty' => 1.75,
            'ReqQtyUnit' => 'uL',
        ]);
        $payload['SiteID'] = null;
        $payload['SeqScr'] = null;
        $payload['SeqRpt'] = null;
        $payload['refnum'] = [
            [
                'NumRefType' => 'NMRC',
                'RangeType' => 'REF',
                'Sex' => '2',
                'LowSign' => 'GE',
                'Low' => 10,
                'HighSign' => 'LE',
                'High' => 20,
                'AgeStart' => null,
                'AgeEnd' => null,
                'Flag' => 'N',
                'Interpretation' => 'Nullable range',
                'SpcType' => 'GEN',
            ],
        ];

        $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
        $response->assertStatus(201);

        $testSiteId = json_decode($response->getJSON(), true)['data']['TestSiteId'];
        $show = $this->call('get', $this->endpoint . '/' . $testSiteId);
        $show->assertStatus(200);

        $data = json_decode($show->getJSON(), true)['data'];
        $this->assertNull($data['SiteID']);
        $this->assertNull($data['SeqScr']);
        $this->assertNull($data['SeqRpt']);
        $this->assertNull($data['Decimal']);
        $this->assertSame(2.5, (float) $data['Factor']);
        $this->assertSame(1.75, (float) $data['ReqQty']);
        $this->assertNull($data['refnum'][0]['AgeStart']);
        $this->assertNull($data['refnum'][0]['AgeEnd']);
    }

    public function testCreateTechnicalWithNumericReference(): void
    {
        $refnum = $this->buildRefNumEntries('NMRC', true);
        foreach (['TEST', 'PARAM'] as $type) {
            $this->assertTechnicalCreated($type, [
                'ResultType' => 'NMRIC',
                'RefType' => 'RANGE',
            ], $refnum);
        }
    }

    public function testCreateTechnicalNumericReferenceReturnsAgeInDays(): void
    {
        $payload = $this->buildTechnicalPayload('TEST', [
            'ResultType' => 'NMRIC',
            'RefType' => 'RANGE',
        ]);

        $payload['refnum'] = [
            [
                'NumRefType' => 'NMRC',
                'RangeType' => 'REF',
                'Sex' => '2',
                'LowSign' => 'GE',
                'Low' => 10,
                'HighSign' => 'LE',
                'High' => 20,
                'AgeStart' => 6570,
                'AgeEnd' => 36135,
                'Flag' => 'N',
                'Interpretation' => 'Adult range in days',
                'SpcType' => 'GEN',
            ],
        ];

        $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
        $response->assertStatus(201);

        $testSiteId = json_decode($response->getJSON(), true)['data']['TestSiteId'];
        $show = $this->call('get', $this->endpoint . '/' . $testSiteId);
        $show->assertStatus(200);

        $data = json_decode($show->getJSON(), true)['data'];
        $this->assertSame(6570, (int) $data['refnum'][0]['AgeStart']);
        $this->assertSame(36135, (int) $data['refnum'][0]['AgeEnd']);
    }

    public function testNumericRefRangeNotesPersistAfterCreate(): void
    {
        $notes = 'Auto note ' . uniqid();
        $refnum = $this->buildRefNumEntries('NMRC', false);
        $refnum[0]['Notes'] = $notes;

        $payload = $this->buildTechnicalPayload('TEST', [
            'ResultType' => 'NMRIC',
            'RefType' => 'RANGE',
        ]);
        $payload['refnum'] = $refnum;

        $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
        $response->assertStatus(201);
        $json = json_decode($response->getJSON(), true);
        $testSiteId = $json['data']['TestSiteId'];

        $show = $this->call('get', $this->endpoint . '/' . $testSiteId);
        $show->assertStatus(200);
        $showJson = json_decode($show->getJSON(), true);
        $this->assertSame($notes, $showJson['data']['refnum'][0]['Notes']);
    }

    public function testNumericRefRangeSpcTypePersistAfterCreate(): void
    {
        $spcType = 'EDTA';
        $refnum = $this->buildRefNumEntries('NMRC', false);
        $refnum[0]['SpcType'] = $spcType;

        $payload = $this->buildTechnicalPayload('TEST', [
            'ResultType' => 'NMRIC',
            'RefType' => 'RANGE',
        ]);
        $payload['refnum'] = $refnum;

        $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
        $response->assertStatus(201);
        $json = json_decode($response->getJSON(), true);
        $testSiteId = $json['data']['TestSiteId'];

        $show = $this->call('get', $this->endpoint . '/' . $testSiteId);
        $show->assertStatus(200);
        $showJson = json_decode($show->getJSON(), true);
        $this->assertSame($spcType, $showJson['data']['refnum'][0]['SpcType']);
    }

    public function testCreateTechnicalWithThresholdReference(): void
    {
        $refnum = $this->buildRefNumEntries('THOLD', true);
        foreach (['TEST', 'PARAM'] as $type) {
            $this->assertTechnicalCreated($type, [
                'ResultType' => 'NMRIC',
                'RefType' => 'THOLD',
            ], $refnum);
        }
    }

    public function testCreateTechnicalWithTextReference(): void
    {
        $reftxt = $this->buildRefTxtEntries('TEXT', true);
        foreach (['TEST', 'PARAM'] as $type) {
            $this->assertTechnicalCreated($type, [
                'ResultType' => 'TEXT',
                'RefType' => 'TEXT',
            ], null, $reftxt);
        }
    }

    public function testCreateTechnicalWithValuesetReference(): void
    {
        $reftxt = $this->buildRefTxtEntries('VSET', true);
        foreach (['TEST', 'PARAM'] as $type) {
            $this->assertTechnicalCreated($type, [
                'ResultType' => 'VSET',
                'RefType' => 'VSET',
            ], null, $reftxt);
        }
    }

    public function testCreateTechnicalWithNumericReferenceAndTestMap(): void
    {
        $refnum = $this->buildRefNumEntries('NMRC', true);
        $testmap = $this->buildTestMap(true, true);
        foreach (['TEST', 'PARAM'] as $type) {
            $this->assertTechnicalCreated($type, [
                'ResultType' => 'NMRIC',
                'RefType' => 'RANGE',
            ], $refnum, null, $testmap);
        }
    }

    public function testCreateTechnicalWithThresholdReferenceAndTestMap(): void
    {
        $refnum = $this->buildRefNumEntries('THOLD', true);
        $testmap = $this->buildTestMap(true, true);
        foreach (['TEST', 'PARAM'] as $type) {
            $this->assertTechnicalCreated($type, [
                'ResultType' => 'NMRIC',
                'RefType' => 'THOLD',
            ], $refnum, null, $testmap);
        }
    }

    public function testCreateTechnicalWithTextReferenceAndTestMap(): void
    {
        $reftxt = $this->buildRefTxtEntries('TEXT', true);
        $testmap = $this->buildTestMap(true, true);
        foreach (['TEST', 'PARAM'] as $type) {
            $this->assertTechnicalCreated($type, [
                'ResultType' => 'TEXT',
                'RefType' => 'TEXT',
            ], null, $reftxt, $testmap);
        }
    }

    public function testCreateTechnicalWithValuesetReferenceAndTestMap(): void
    {
        $reftxt = $this->buildRefTxtEntries('VSET', true);
        $testmap = $this->buildTestMap(true, true);
        foreach (['TEST', 'PARAM'] as $type) {
            $this->assertTechnicalCreated($type, [
                'ResultType' => 'VSET',
                'RefType' => 'VSET',
            ], null, $reftxt, $testmap);
        }
    }

    public function testCreateTechnicalValuesetWithoutReferenceButWithMap(): void
    {
        $testmap = $this->buildTestMap(false, true);
        foreach (['TEST', 'PARAM'] as $type) {
            $this->assertTechnicalCreated($type, [
                'ResultType' => 'VSET',
                'RefType' => 'VSET',
            ], null, null, $testmap);
        }
    }

    public function testCreateTechnicalWithFlatTestMapPayload(): void
    {
        $testCode = $this->generateTestCode('TEST');
        $payload = [
            'SiteID' => self::SITE_ID,
            'TestSiteCode' => $testCode,
            'TestSiteName' => 'Auto Flat Map Create',
            'TestType' => 'TEST',
            'isVisibleScr' => 1,
            'isVisibleRpt' => 1,
            'isCountStat' => 1,
            'details' => [
                'ResultType' => 'NMRIC',
                'RefType' => 'RANGE',
            ],
            'testmap' => $this->buildFlatTestMap($testCode),
        ];

        $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
        $response->assertStatus(201);

        $createdId = json_decode($response->getJSON(), true)['data']['TestSiteId'];
        $show = $this->call('get', $this->endpoint . '/' . $createdId);
        $show->assertStatus(200);

        $data = json_decode($show->getJSON(), true)['data'];
        $this->assertArrayNotHasKey('testmap', $data);
    }

    public function testPatchTechnicalWithFlatTestMapPayload(): void
    {
        $payload = $this->buildTechnicalPayload('TEST', [
            'ResultType' => 'NMRIC',
            'RefType' => 'RANGE',
        ]);

        $create = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
        $create->assertStatus(201);

        $createJson = json_decode($create->getJSON(), true);
        $testSiteId = $createJson['data']['TestSiteId'];
        $testCode = $payload['TestSiteCode'];

        $patchPayload = [
            'details' => [
                'ResultType' => 'NMRIC',
                'RefType' => 'RANGE',
            ],
            'testmap' => $this->buildFlatTestMap($testCode),
        ];

        $patch = $this->withBodyFormat('json')->call('patch', $this->endpoint . '/' . $testSiteId, $patchPayload);
        $patch->assertStatus(200);

        $show = $this->call('get', $this->endpoint . '/' . $testSiteId);
        $show->assertStatus(200);

        $data = json_decode($show->getJSON(), true)['data'];
        $this->assertArrayNotHasKey('testmap', $data);
    }

    public function testCreateCalculatedTestWithoutReferenceOrMap(): void
    {
        $this->assertCalculatedCreated(false);
    }

    public function testCreateCalculatedTestWithReferenceAndTestMap(): void
    {
        $refnum = $this->buildRefNumEntries('NMRC', true);
        $testmap = $this->buildTestMap(true, true);
        $members = $this->resolveMemberIds(['GLU', 'CREA']);
        $this->assertCalculatedCreated(true, $refnum, $testmap, $members);
    }

    public function testCreateGroupTestWithMembers(): void
    {
        $members = $this->resolveMemberIds(['GLU', 'CREA']);
        $testmap = $this->buildTestMap(true, true);
        $payload = $this->buildGroupPayload($members, $testmap);

        $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
        $response->assertStatus(201);
        $response->assertJSONFragment([
            'status' => 'created',
            'message' => 'Test created successfully',
        ]);

        $json = json_decode($response->getJSON(), true);
        $this->assertArrayHasKey('data', $json);
        $this->assertArrayHasKey('TestSiteId', $json['data']);
        $this->assertIsInt($json['data']['TestSiteId']);

        $show = $this->call('get', $this->endpoint . '/' . $json['data']['TestSiteId']);
        $show->assertStatus(200);

        $showData = json_decode($show->getJSON(), true)['data'];
        $this->assertArrayHasKey('testdefgrp', $showData);
        $this->assertArrayHasKey('members', $showData['testdefgrp']);
        $this->assertArrayNotHasKey('members', $showData);

        if ($members !== []) {
            $this->assertCount(count($members), $showData['testdefgrp']['members']);
        }
    }

    private function assertTechnicalCreated(
        string $type,
        array $details = [],
        ?array $refnum = null,
        ?array $reftxt = null,
        ?array $testmap = null
    ): void {
        $payload = $this->buildTechnicalPayload($type, $details);
        if ($refnum !== null) {
            $payload['refnum'] = $refnum;
        }
        if ($reftxt !== null) {
            $payload['reftxt'] = $reftxt;
        }
        if ($testmap !== null) {
            $payload['testmap'] = $testmap;
        }

        $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
        $response->assertStatus(201);
        $response->assertJSONFragment([
            'status' => 'created',
            'message' => 'Test created successfully',
        ]);

        $json = json_decode($response->getJSON(), true);
        $this->assertArrayHasKey('data', $json);
        $this->assertArrayHasKey('TestSiteId', $json['data']);
        $this->assertIsInt($json['data']['TestSiteId']);

    }

    private function assertCalculatedCreated(
        bool $withDetails,
        ?array $refnum = null,
        ?array $testmap = null,
        array $members = []
    ): void {
        $payload = $this->buildCalculatedPayload($members);

        if ($withDetails && $refnum !== null) {
            $payload['refnum'] = $refnum;
        }
        if ($withDetails && $testmap !== null) {
            $payload['testmap'] = $testmap;
        }

        $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload);
        $response->assertStatus(201);
        $response->assertJSONFragment([
            'status' => 'created',
            'message' => 'Test created successfully',
        ]);

        $json = json_decode($response->getJSON(), true);
        $this->assertArrayHasKey('data', $json);
        $this->assertArrayHasKey('TestSiteId', $json['data']);
        $this->assertIsInt($json['data']['TestSiteId']);

        $show = $this->call('get', $this->endpoint . '/' . $json['data']['TestSiteId']);
        $show->assertStatus(200);

        $showData = json_decode($show->getJSON(), true)['data'];
        $this->assertArrayHasKey('testdefgrp', $showData);
        $this->assertArrayHasKey('members', $showData['testdefgrp']);
        $this->assertArrayNotHasKey('members', $showData);
        $this->assertCount(count($members), $showData['testdefgrp']['members']);
    }

    private function buildTechnicalPayload(string $testType, array $details = []): array
    {
        $payload = [
            'SiteID' => self::SITE_ID,
            'TestSiteCode' => $this->generateTestCode($testType),
            'TestSiteName' => 'Auto ' . strtoupper($testType),
            'TestType' => $testType,
            'SeqScr' => 900,
            'SeqRpt' => 900,
            'isVisibleScr' => 1,
            'isVisibleRpt' => 1,
            'isCountStat' => 1,
        ];

        $payload['details'] = $this->normalizeDetails($details);

        return $payload;
    }

    private function buildCalculatedPayload(array $members = []): array
    {
        $payload = [
            'SiteID' => self::SITE_ID,
            'TestSiteCode' => $this->generateTestCode('CALC'),
            'TestSiteName' => 'Auto CALC',
            'TestType' => 'CALC',
            'SeqScr' => 1000,
            'SeqRpt' => 1000,
            'isVisibleScr' => 1,
            'isVisibleRpt' => 1,
            'isCountStat' => 0,
            'testdefgrp' => [
                'members' => array_map(fn ($id) => ['TestSiteID' => $id], $members),
            ],
            'details' => [
                'DisciplineID' => 2,
                'DepartmentID' => 2,
                'FormulaCode' => '{GLU} + {CREA}',
            ],
        ];

        return $payload;
    }

    private function buildGroupPayload(array $members, array $testmap): array
    {
        return [
            'SiteID' => self::SITE_ID,
            'TestSiteCode' => $this->generateTestCode('PANEL'),
            'TestSiteName' => 'Auto Group',
            'TestType' => 'GROUP',
            'SeqScr' => 300,
            'SeqRpt' => 300,
            'isVisibleScr' => 1,
            'isVisibleRpt' => 1,
            'isCountStat' => 1,
            'testdefgrp' => [
                'members' => array_map(fn ($id) => ['TestSiteID' => $id], $members),
            ],
            'testmap' => $testmap,
        ];
    }

    private function normalizeDetails(array $details): array
    {
        $normalized = [
            'DisciplineID' => $details['DisciplineID'] ?? 2,
            'DepartmentID' => $details['DepartmentID'] ?? 2,
            'Method' => $details['Method'] ?? 'Automated test',
            'Unit1' => $details['Unit1'] ?? 'mg/dL',
            'Decimal' => array_key_exists('Decimal', $details) ? $details['Decimal'] : 0,
        ];

        foreach (['ResultType', 'RefType', 'FormulaCode', 'ExpectedTAT', 'Factor', 'ReqQty', 'ReqQtyUnit', 'Unit2', 'VSet', 'CollReq'] as $key) {
            if (array_key_exists($key, $details)) {
                $normalized[$key] = $details[$key];
            }
        }

        return $normalized;
    }

    private function buildRefNumEntries(string $numRefType, bool $multiple = false): array
    {
        $rangeType = $numRefType === 'THOLD' ? 'PANIC' : 'REF';
        $entries = [
            [
                'NumRefType' => $numRefType,
                'RangeType' => $rangeType,
                'Sex' => '2',
                'LowSign' => 'GE',
                'Low' => 10,
                'HighSign' => 'LE',
                'High' => $numRefType === 'THOLD' ? 40 : 20,
                'AgeStart' => 0,
                'AgeEnd' => 43800,
                'Flag' => 'N',
                'Interpretation' => 'Normal range',
                'SpcType' => 'GEN',
                'Notes' => 'Default numeric range note',
            ],
        ];

        if ($multiple) {
            $entries[] = [
                'NumRefType' => $numRefType,
                'RangeType' => $rangeType,
                'Sex' => '1',
                'LowSign' => '>',
                'Low' => 5,
                'HighSign' => '<',
                'High' => $numRefType === 'THOLD' ? 50 : 15,
                'AgeStart' => 0,
                'AgeEnd' => 36135,
                'Flag' => 'N',
                'Interpretation' => 'Alternate range',
                'SpcType' => 'GEN',
                'Notes' => 'Alternate numeric range note',
            ];
        }

        return $entries;
    }

    private function buildRefTxtEntries(string $txtRefType, bool $multiple = false): array
    {
        $entries = [
            [
                'SpcType' => 'GEN',
                'TxtRefType' => $txtRefType,
                'Sex' => '2',
                'AgeStart' => 0,
                'AgeEnd' => 43800,
                'RefTxt' => $txtRefType === 'VSET' ? 'NORM=Normal;ABN=Abnormal' : 'NORM=Normal',
                'Flag' => 'N',
            ],
        ];

        if ($multiple) {
            $entries[] = [
                'SpcType' => 'GEN',
                'TxtRefType' => $txtRefType,
                'Sex' => '1',
                'AgeStart' => 0,
                'AgeEnd' => 43800,
                'RefTxt' => $txtRefType === 'VSET' ? 'HIGH=High;LOW=Low' : 'ABN=Abnormal',
                'Flag' => 'N',
            ];
        }

        return $entries;
    }

    private function buildTestMap(bool $multipleMaps = false, bool $multipleDetails = false): array
    {
        $map = [
            [
                'HostType' => 'SITE',
                'HostID' => '1',
                'ClientType' => 'WST',
                'ClientID' => '1',
                'details' => [
                    [
                        'HostTestCode' => 'GLU',
                        'HostTestName' => 'Glucose',
                        'ConDefID' => 1,
                        'ClientTestCode' => 'GLU_C',
                        'ClientTestName' => 'Glucose Client',
                    ],
                ],
            ],
        ];

        if ($multipleDetails) {
            $map[0]['details'][] = [
                'HostTestCode' => 'CREA',
                'HostTestName' => 'Creatinine',
                'ConDefID' => 2,
                'ClientTestCode' => 'CREA_C',
                'ClientTestName' => 'Creatinine Client',
            ];
        }

        if ($multipleMaps) {
            $map[] = [
                'HostType' => 'WST',
                'HostID' => '3',
                'ClientType' => 'INST',
                'ClientID' => '2',
                'details' => [
                    [
                        'HostTestCode' => 'HB',
                        'HostTestName' => 'Hemoglobin',
                        'ConDefID' => 3,
                        'ClientTestCode' => 'HB_C',
                        'ClientTestName' => 'Hemoglobin Client',
                    ],
                ],
            ];
        }

        return $map;
    }

    private function buildFlatTestMap(string $testCode): array
    {
        return [
            [
                'HostType' => 'HIS',
                'HostID' => 'LOKAL',
                'HostTestCode' => $testCode,
                'HostTestName' => 'Host ' . $testCode,
                'ClientType' => 'SITE',
                'ClientID' => '1',
                'ClientTestCode' => $testCode,
                'ClientTestName' => 'Client ' . $testCode,
                'ConDefID' => null,
            ],
        ];
    }

    private function generateTestCode(string $prefix): string
    {
        $clean = strtoupper(substr($prefix, 0, 3));
        $suffix = strtoupper(substr(md5((string) microtime(true) . random_int(0, 9999)), 0, 6));
        return substr($clean . $suffix, 0, 10);
    }

    private function resolveMemberIds(array $codes): array
    {
        $ids = [];
        foreach ($codes as $code) {
            $row = $this->testModel->where('TestSiteCode', $code)->where('EndDate IS NULL')->first();
            $this->assertNotEmpty($row, "Seeded test code {$code} not found");
            $ids[] = (int) $row['TestSiteID'];
        }
        return $ids;
    }
}
