<?php

app_require('routes/InvoicingPeriodRoute.php');

use routes\InvoicingPeriodRoute;

function invoicing_period_draft_overlay_invoke(string $method, array $args = []): mixed
{
    $reflection = new ReflectionClass(InvoicingPeriodRoute::class);
    $target = $reflection->getMethod($method);
    return $target->invokeArgs(null, $args);
}

function invoicing_period_draft_overlay_draft(
    int $invoice_collection_id,
    int $customer_number,
    bool $is_period_relevant = true
): array {
    return [
        'invoice_collection_id' => $invoice_collection_id,
        'customer_number' => $customer_number,
        'created_at' => '2026-04-01 00:00:01',
        'closed_at' => '2026-04-01 00:00:01',
        'is_period_relevant' => $is_period_relevant,
    ];
}

function invoicing_period_draft_overlay_transaction(
    int $id,
    ?int $invoice_collection_id,
    bool $booked = false,
    bool $excluded = false,
    ?string $queue_status = null
): array {
    return [
        'id' => $id,
        'booked' => $booked,
        'excluded' => $excluded,
        'invoice_collection_id' => $invoice_collection_id,
        'queue_status' => $queue_status,
        'queue_job_id' => $queue_status === null ? null : 99,
    ];
}

function invoicing_period_draft_overlay_reset_deleted_at_column_cache(): void
{
    $reflection = new ReflectionClass(InvoicingPeriodRoute::class);
    $property = $reflection->getProperty('collectedOrderInvoicesHasDeletedAtColumn');
    $property->setValue(null, null);
}

class InvoicingPeriodDraftOverlayFakeDbResult
{
    public int $num_rows;

    public function __construct(private array $rows = [])
    {
        $this->num_rows = count($rows);
    }

    public function fetch_assoc(): ?array
    {
        return array_shift($this->rows);
    }
}

class InvoicingPeriodDraftOverlayFakeDb
{
    public string $selectSql = '';

    public function __construct(private bool $hasDeletedAtColumn)
    {
    }

    public function escape_string(string $value): string
    {
        return addslashes($value);
    }

    public function query(string $sql): InvoicingPeriodDraftOverlayFakeDbResult|bool
    {
        if (str_starts_with($sql, 'SHOW COLUMNS')) {
            return new InvoicingPeriodDraftOverlayFakeDbResult(
                $this->hasDeletedAtColumn ? [['Field' => 'deleted_at']] : []
            );
        }

        $this->selectSql = $sql;
        return false;
    }
}

it('blocks invoicing when all actionable transactions are backed by valid e-conomic drafts', function (): void {
    $draft = invoicing_period_draft_overlay_draft(14578, 42424242);

    $customer = [
        'customer_number' => 42424242,
        'customer_name' => 'Draft Customer',
        'requires_action' => true,
        'transactions' => [
            invoicing_period_draft_overlay_transaction(501, 14578),
        ],
    ];

    $result = invoicing_period_draft_overlay_invoke('applyCollectedInvoiceDraftOverlayToCustomer', [
        $customer,
        [14578 => $draft],
        [42424242 => [$draft]],
    ]);

    expect($result['requires_action'])->toBeFalse();
    expect($result['queue'])->toBe([
        'has_active_job' => false,
        'statuses' => [],
        'invoice_collection_ids' => [],
        'is_action_blocked' => false,
    ]);
    expect($result['draft'])->toBe([
        'has_valid_draft' => true,
        'invoice_collection_ids' => [14578],
        'is_action_blocked' => true,
    ]);
});

it('keeps invoicing available when valid drafts only cover part of the actionable work', function (): void {
    $draft = invoicing_period_draft_overlay_draft(2001, 43434343);

    $customer = [
        'customer_number' => 43434343,
        'customer_name' => 'Mixed Draft Customer',
        'requires_action' => true,
        'transactions' => [
            invoicing_period_draft_overlay_transaction(601, 2001),
            invoicing_period_draft_overlay_transaction(602, null),
        ],
    ];

    $result = invoicing_period_draft_overlay_invoke('applyCollectedInvoiceDraftOverlayToCustomer', [
        $customer,
        [2001 => $draft],
        [43434343 => [$draft]],
    ]);

    expect($result['requires_action'])->toBeTrue();
    expect($result['draft'])->toBe([
        'has_valid_draft' => true,
        'invoice_collection_ids' => [2001],
        'is_action_blocked' => false,
    ]);
});

it('excludes errored, booked, deleted, and missing-external-id invoice collections at query time', function (): void {
    $hadDb = array_key_exists('db', $GLOBALS);
    $originalDb = $GLOBALS['db'] ?? null;
    $fakeDb = new InvoicingPeriodDraftOverlayFakeDb(true);
    invoicing_period_draft_overlay_reset_deleted_at_column_cache();
    $GLOBALS['db'] = $fakeDb;

    try {
        invoicing_period_draft_overlay_invoke('getValidCollectedInvoiceDraftOverlay', [
            [
                'all' => [
                    [
                        'customer_number' => 45454545,
                        'transactions' => [
                            invoicing_period_draft_overlay_transaction(701, 3001),
                        ],
                        'meta' => [
                            'fixed_pricing' => [
                                'price' => 1200,
                            ],
                        ],
                    ],
                ],
            ],
            '2026-04-01',
            '2026-04-30',
        ]);
    } finally {
        if ($hadDb) {
            $GLOBALS['db'] = $originalDb;
        } else {
            unset($GLOBALS['db']);
        }
        invoicing_period_draft_overlay_reset_deleted_at_column_cache();
    }

    expect($fakeDb->selectSql)->toContain('deleted_at IS NULL');
    expect($fakeDb->selectSql)->toContain('processor = 1');
    expect($fakeDb->selectSql)->toContain('external_id IS NOT NULL');
    expect($fakeDb->selectSql)->toContain("external_id <> ''");
    expect($fakeDb->selectSql)->toContain('booked_invoice_id IS NULL');
    expect($fakeDb->selectSql)->toContain('error_message IS NULL');
});

it('still checks valid drafts when the collection table has no deleted marker column', function (): void {
    $hadDb = array_key_exists('db', $GLOBALS);
    $originalDb = $GLOBALS['db'] ?? null;
    $fakeDb = new InvoicingPeriodDraftOverlayFakeDb(false);
    invoicing_period_draft_overlay_reset_deleted_at_column_cache();
    $GLOBALS['db'] = $fakeDb;

    try {
        invoicing_period_draft_overlay_invoke('getValidCollectedInvoiceDraftOverlay', [
            [
                'all' => [
                    [
                        'customer_number' => 12345679,
                        'transactions' => [
                            invoicing_period_draft_overlay_transaction(61415, 17389),
                        ],
                    ],
                ],
            ],
            '2026-05-11',
            '2026-05-11',
        ]);
    } finally {
        if ($hadDb) {
            $GLOBALS['db'] = $originalDb;
        } else {
            unset($GLOBALS['db']);
        }
        invoicing_period_draft_overlay_reset_deleted_at_column_cache();
    }

    expect($fakeDb->selectSql)->not->toContain('deleted_at IS NULL');
    expect($fakeDb->selectSql)->toContain('id IN (17389)');
    expect($fakeDb->selectSql)->toContain('processor = 1');
    expect($fakeDb->selectSql)->toContain('error_message IS NULL');
});

it('blocks fixed-pricing and subscription customer-level work when a relevant valid draft exists', function (): void {
    $draft = invoicing_period_draft_overlay_draft(3001, 45454545, true);

    $customer = [
        'customer_number' => 45454545,
        'customer_name' => 'Subscription Draft Customer',
        'requires_action' => true,
        'transactions' => [],
        'meta' => [
            'fixed_pricing' => [
                'price' => 1200,
            ],
        ],
    ];

    $result = invoicing_period_draft_overlay_invoke('applyCollectedInvoiceDraftOverlayToCustomer', [
        $customer,
        [],
        [45454545 => [$draft]],
    ]);

    expect($result['requires_action'])->toBeFalse();
    expect($result['draft'])->toBe([
        'has_valid_draft' => true,
        'invoice_collection_ids' => [3001],
        'is_action_blocked' => true,
    ]);
});

it('keeps queue blocking ahead of the draft label when all work is covered by queue or draft state', function (): void {
    $draft = invoicing_period_draft_overlay_draft(5002, 46464646, true);

    $customer = [
        'customer_number' => 46464646,
        'customer_name' => 'Queue And Draft Customer',
        'requires_action' => true,
        'transactions' => [
            invoicing_period_draft_overlay_transaction(801, 5001, false, false, 'QUEUED'),
            invoicing_period_draft_overlay_transaction(802, 5002),
        ],
        'queue' => [
            'has_active_job' => true,
            'statuses' => ['QUEUED'],
            'invoice_collection_ids' => [5001],
            'is_action_blocked' => false,
        ],
    ];

    $result = invoicing_period_draft_overlay_invoke('applyCollectedInvoiceDraftOverlayToCustomer', [
        $customer,
        [5002 => $draft],
        [46464646 => [$draft]],
    ]);

    expect($result['requires_action'])->toBeFalse();
    expect($result['queue']['is_action_blocked'])->toBeTrue();
    expect($result['draft'])->toBe([
        'has_valid_draft' => true,
        'invoice_collection_ids' => [5002],
        'is_action_blocked' => false,
    ]);
});
