<?php

app_require('classes/invoice_period_flag_service.php');

use classes\invoice_period_flag_service;

function invoice_period_flag_service_instance(): invoice_period_flag_service
{
    $reflection = new ReflectionClass(invoice_period_flag_service::class);
    /** @var invoice_period_flag_service $service */
    $service = $reflection->newInstanceWithoutConstructor();
    return $service;
}

function invoice_period_flag_service_invoke(string $method, array $args = []): mixed
{
    $service = invoice_period_flag_service_instance();
    $reflection = new ReflectionClass(invoice_period_flag_service::class);
    $target = $reflection->getMethod($method);
    return $target->invokeArgs($service, $args);
}

it('normalizes invoice period date ranges to full-day timestamps before cache-backed reads', function (): void {
    expect(invoice_period_flag_service_invoke('normalizePeriodDateRange', ['2025-03-01', '2025-03-31']))
        ->toBe(['2025-03-01 00:00:00', '2025-03-31 23:59:59']);

    expect(invoice_period_flag_service_invoke('normalizePeriodDateRange', [
        '2025-03-01 00:00:00',
        '2025-03-31 23:59:59',
    ]))->toBe(['2025-03-01 00:00:00', '2025-03-31 23:59:59']);
});

it('canonicalizes invoice period redis keys for bare dates and normalized API timestamps', function (): void {
    $reflection = new ReflectionClass(\classes\redis::class);
    $redis = $reflection->newInstanceWithoutConstructor();
    $key = $reflection->getMethod('invoicePeriodCacheKey');

    expect($key->invoke($redis, 'invoice_period_automatic_flags', '2025-03-01', '2025-03-31'))
        ->toBe($key->invoke(
            $redis,
            'invoice_period_automatic_flags',
            '2025-03-01 00:00:00',
            '2025-03-31 23:59:59'
        ));
});

it('builds deterministic automatic flag fingerprints and interactive price message parts', function (): void {
    $row = [
        'customer_number' => 424242,
        'customer_name' => 'Flagged Customer',
        'order_id' => 9001,
        'order_item_id' => 7001,
        'invoice_collection_id' => 3001,
    ];
    $params = [
        'product' => 'Spot Free',
        'expected_price' => 81,
        'actual_price' => 99,
    ];
    $context = [
        'department_id' => 1,
        'order_id' => 9001,
        'order_item_id' => 7001,
    ];

    $flag = invoice_period_flag_service_invoke('automaticFlag', [
        'price_mismatch',
        'order_item_field',
        7001,
        'price',
        $row,
        $params,
        $context,
    ]);
    $sameFlag = invoice_period_flag_service_invoke('automaticFlag', [
        'price_mismatch',
        'order_item_field',
        7001,
        'price',
        $row,
        $params,
        $context,
    ]);
    $changedPriceFlag = invoice_period_flag_service_invoke('automaticFlag', [
        'price_mismatch',
        'order_item_field',
        7001,
        'price',
        $row,
        [
            ...$params,
            'actual_price' => 100,
        ],
        $context,
    ]);

    expect($flag['fingerprint'])->toBe($sameFlag['fingerprint']);
    expect($flag['fingerprint'])->not->toBe($changedPriceFlag['fingerprint']);
    expect($flag['id'])->toBe('auto:' . $flag['fingerprint']);
    expect($flag['message_key'])->toBe('invoice_period.flags.automatic.price_mismatch');
    expect($flag['message'])->toBe('Spot Free product price differs from expected.');
    expect($flag['message_parts'])->toBe([
        ['type' => 'order_item', 'text' => 'Spot Free'],
        ['type' => 'text', 'text' => ' product price differs from '],
        ['type' => 'expected_price', 'text' => 'expected'],
        ['type' => 'text', 'text' => '.'],
    ]);
});

it('builds interactive message parts for order and wash certificate warnings', function (): void {
    $row = [
        'customer_number' => 424242,
        'customer_name' => 'Flagged Customer',
        'order_id' => 9001,
        'order_item_id' => 7001,
        'invoice_collection_id' => 3001,
    ];

    $orderFlag = invoice_period_flag_service_invoke('automaticFlag', [
        'multiple_identical_primary_vehicle_items',
        'order',
        9001,
        null,
        $row,
        [],
        ['department_id' => 1, 'order_id' => 9001],
    ]);
    $washCertificateFlag = invoice_period_flag_service_invoke('automaticFlag', [
        'wash_certificate_item_without_certificate',
        'order_item',
        7001,
        null,
        $row,
        [],
        ['department_id' => 1, 'order_id' => 9001, 'order_item_id' => 7001],
    ]);
    $xlVaskFlag = invoice_period_flag_service_invoke('automaticFlag', [
        'xlvask_missing_order_link',
        'xlvask_usage_log',
        55,
        null,
        [
            'customer_number' => 424242,
            'customer_name' => 'Flagged Customer',
            'xlvask_usage_log_id' => 55,
        ],
        [
            'wash_id' => 'wash-55',
            'registration_number' => 'AB12345',
        ],
        [
            'customer_number' => 424242,
            'customer_name' => 'Flagged Customer',
            'xlvask_usage_log_id' => 55,
            'wash_id' => 'wash-55',
            'registration_number' => 'AB12345',
            'start_time' => '2026-05-11 10:00:00',
        ],
    ]);

    expect($orderFlag['message_parts'])->toBe([
        ['type' => 'order', 'text' => 'Order'],
        ['type' => 'text', 'text' => ' contains multiple identical primary vehicle items.'],
    ]);
    expect($washCertificateFlag['message_parts'])->toBe([
        ['type' => 'order_item', 'text' => 'Wash certificate item'],
        ['type' => 'text', 'text' => ' is present without a wash certificate.'],
    ]);
    expect($xlVaskFlag['message_parts'])->toBe([
        ['type' => 'xlvask_usage_log', 'text' => 'XL Vask wash'],
        ['type' => 'text', 'text' => ' is neither ignored nor linked to an order in the selected period.'],
    ]);
});

it('includes order item preview context for required order field warnings', function (): void {
    global $db;

    $hadDb = array_key_exists('db', $GLOBALS);
    $previousDb = $GLOBALS['db'] ?? null;
    $db = new class {
        public array $queries = [];

        public function query(string $sql): object|false
        {
            $this->queries[] = $sql;

            if (str_contains($sql, 'FROM order_items')) {
                return $this->result([
                    [
                        'id' => 91,
                        'order_id' => 61415,
                        'product_id' => 3,
                        'reference' => '',
                        'notes' => '',
                        'price' => 649,
                        'quantity' => 1,
                        'related_item_id' => 0,
                        'product_name' => 'Forvogn',
                        'product_base_price' => 649,
                    ],
                    [
                        'id' => 92,
                        'order_id' => 61415,
                        'product_id' => 4,
                        'reference' => '',
                        'notes' => '',
                        'price' => 599,
                        'quantity' => 1,
                        'related_item_id' => 0,
                        'product_name' => 'Trailer',
                        'product_base_price' => 599,
                    ],
                ]);
            }

            return false;
        }

        public function fetch_all(object $result): array
        {
            return $result->rows;
        }

        private function result(array $rows): object
        {
            return new class($rows) {
                public int $num_rows;
                public array $rows;

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

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

    try {
        $row = [
            'customer_number' => 424242,
            'customer_name' => 'Flagged Customer',
            'order_id' => 61415,
            'order_item_id' => 91,
            'invoice_collection_id' => 3001,
            'department_id' => 5,
            'order_reference' => '',
            'order_po' => '',
            'reg_1' => 'EC21233',
        ];

        $flags = invoice_period_flag_service_invoke('detectCustomerRuleViolations', [
            [$row],
            [424242 => ['requiresReferenceNumber' => true, 'usePONumbers' => true]],
        ]);
        $byDefinition = [];
        foreach ($flags as $flag) {
            $byDefinition[$flag['definition_key']] = $flag;
        }

        expect($byDefinition['customer_rule_requires_reference'] ?? null)->not->toBeNull();
        expect($byDefinition['customer_rule_requires_po_number'] ?? null)->not->toBeNull();
        expect($byDefinition['customer_rule_requires_reference']['context']['order_items'])->toHaveCount(2);
        expect($byDefinition['customer_rule_requires_reference']['context']['order_items'][0]['product_name'])->toBe('Forvogn');
        expect($byDefinition['customer_rule_requires_po_number']['context']['order_items'][1]['product_name'])->toBe('Trailer');
    } finally {
        if ($hadDb) {
            $db = $previousDb;
        } else {
            unset($GLOBALS['db']);
        }
    }
});

it('allows tank cleaning products for only tank cleaning customers', function (): void {
    $baseRow = [
        'customer_number' => 424242,
        'customer_name' => 'Tank Customer',
        'order_id' => 61426,
        'invoice_collection_id' => 3090,
        'department_id' => 5,
        'related_item_id' => 0,
        'item_price' => 100,
        'order_reference' => 'REF',
        'order_po' => 'PO',
        'reg_1' => 'NI465',
    ];
    $tankCleaningRow = $baseRow + [
        'order_item_id' => 801,
        'product_id' => 30,
        'product_name' => 'Tank cleaning 4 spulehoveder',
        'product_category' => 5,
        'category_name' => 'Tank cleaning',
    ];
    $tankCleaningAddonRow = $baseRow + [
        'order_item_id' => 802,
        'product_id' => 33,
        'product_name' => 'Saebe/kemi, 1-4 spulehoveder',
        'product_category' => 5,
        'category_name' => 'Tank cleaning',
    ];
    $washRow = $baseRow + [
        'order_item_id' => 803,
        'product_id' => 3,
        'product_name' => 'Forvogn',
        'product_category' => 1,
        'category_name' => 'Vask',
    ];

    $onlyTankCleaningFlags = invoice_period_flag_service_invoke('detectCustomerRuleViolations', [
        [$tankCleaningRow, $tankCleaningAddonRow, $washRow],
        [424242 => [
            'onlyTankCleaning' => true,
            '__disabled_products' => [3 => ['onlyTankCleaning' => true]],
        ]],
    ]);

    expect(array_column($onlyTankCleaningFlags, 'definition_key'))->toBe(['customer_rule_only_tank_cleaning']);
    expect($onlyTankCleaningFlags[0]['target_id'])->toBe(803);

    $restrictedTankCleaningFlags = invoice_period_flag_service_invoke('detectCustomerRuleViolations', [
        [$tankCleaningRow, $washRow],
        [424242 => [
            'restrictTankCleaning' => true,
            '__disabled_products' => [30 => ['restrictTankCleaning' => true]],
        ]],
    ]);

    expect(array_column($restrictedTankCleaningFlags, 'definition_key'))->toBe(['customer_rule_restrict_tank_cleaning']);
    expect($restrictedTankCleaningFlags[0]['target_id'])->toBe(801);
});

it('flags every exactly configured product regardless of standalone or addon context', function (): void {
    $baseRow = [
        'customer_number' => 424242,
        'customer_name' => 'Additional Services Customer',
        'order_id' => 61427,
        'invoice_collection_id' => 3091,
        'department_id' => 5,
        'item_price' => 100,
        'order_reference' => 'REF',
        'order_po' => 'PO',
        'reg_1' => 'AB12345',
    ];
    $standaloneCategoryEight = $baseRow + [
        'order_item_id' => 811,
        'product_id' => 91,
        'product_name' => 'Extra detergent',
        'product_category' => 8,
        'category_name' => 'Tillægsydelser',
        'related_item_id' => null,
    ];
    $relatedCategoryEight = $baseRow + [
        'order_item_id' => 812,
        'product_id' => 71,
        'product_name' => 'Interior rinse',
        'product_category' => 8,
        'category_name' => 'Tillægsydelser',
        'related_item_id' => 810,
    ];
    $namedCategoryFourAddon = $baseRow + [
        'order_item_id' => 813,
        'product_id' => 63,
        'product_name' => 'Trailer add-on',
        'product_category' => 4,
        'category_name' => 'Addons',
        'related_item_id' => null,
    ];
    $legacyCategoryName = $baseRow + [
        'order_item_id' => 814,
        'product_id' => 94,
        'product_name' => 'Legacy additional service',
        'product_category' => 18,
        'category_name' => 'TILLÆGSYDELSER',
        'related_item_id' => null,
    ];

    $flags = invoice_period_flag_service_invoke('detectCustomerRuleViolations', [
        [$standaloneCategoryEight, $relatedCategoryEight, $namedCategoryFourAddon, $legacyCategoryName],
        [424242 => [
            'restrictAdditionalServices' => true,
            '__disabled_products' => [
                91 => ['restrictAdditionalServices' => true],
                71 => ['restrictAdditionalServices' => true],
                94 => ['restrictAdditionalServices' => true],
            ],
        ]],
    ]);

    expect(array_column($flags, 'definition_key'))->toBe([
        'customer_rule_restrict_addon_services',
        'customer_rule_restrict_addon_services',
        'customer_rule_restrict_addon_services',
    ]);
    expect(array_column($flags, 'target_id'))->toBe([811, 812, 814]);
});

it('does not flag interior wash variants as historical primary product mismatches', function (): void {
    global $db;

    $hadDb = array_key_exists('db', $GLOBALS);
    $previousDb = $GLOBALS['db'] ?? null;
    $db = new class {
        public function escape_string(string $value): string
        {
            return addslashes($value);
        }

        public function query(string $sql): object|false
        {
            if (str_contains($sql, 'SHOW COLUMNS FROM `customer_vehicles`')) {
                return $this->result([]);
            }

            if (str_contains($sql, 'FROM customer_vehicles')) {
                return $this->result([]);
            }

            if (str_contains($sql, 'FROM orders o') && str_contains($sql, 'GROUP BY UPPER(TRIM(o.reg_1))')) {
                return $this->result([
                    [
                        'reg' => 'CN96636',
                        'product_id' => 3,
                        'product_name' => 'Forvogn',
                        'usage_count' => 5,
                    ],
                ]);
            }

            return false;
        }

        private function result(array $rows): object
        {
            return new class($rows) {
                public int $num_rows;
                public array $rows;

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

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

    try {
        $baseRow = [
            'customer_number' => 424242,
            'customer_name' => 'History Customer',
            'order_id' => 61311,
            'order_item_id' => 901,
            'invoice_collection_id' => 16912,
            'department_id' => 7,
            'reg_1' => 'CN96636',
            'is_wash' => 1,
            'related_item_id' => 0,
            'order_created_at' => '2026-05-11 08:05:21',
        ];

        $interiorVariantFlags = invoice_period_flag_service_invoke('detectVehicleTypeMismatches', [
            [
                $baseRow + [
                    'product_id' => 99,
                    'product_name' => 'Indvendig vask Forvogn',
                ],
            ],
            '2026-05-11 00:00:00',
        ]);

        expect($interiorVariantFlags)->toBe([]);

        $mismatchFlags = invoice_period_flag_service_invoke('detectVehicleTypeMismatches', [
            [
                $baseRow + [
                    'product_id' => 17,
                    'product_name' => 'Bus',
                ],
            ],
            '2026-05-11 00:00:00',
        ]);

        expect(array_column($mismatchFlags, 'definition_key'))->toBe(['historical_primary_product_mismatch']);
    } finally {
        if ($hadDb) {
            $db = $previousDb;
        } else {
            unset($GLOBALS['db']);
        }
    }
});

it('does not report duplicate primary vehicle products from duplicated detector rows for the same order item', function (): void {
    $row = [
        'customer_number' => 424242,
        'customer_name' => 'Flagged Customer',
        'order_id' => 9001,
        'order_item_id' => 7001,
        'product_id' => 3,
        'product_name' => 'Forvogn',
        'category_name' => 'Vask',
        'is_wash' => 1,
        'related_item_id' => 0,
        'item_quantity' => 1,
        'max_quantity_per_order' => null,
        'safety_seal' => '',
        'order_created_at' => '2026-05-11 10:00:00',
        'reg_1' => 'AB12345',
    ];

    $flags = invoice_period_flag_service_invoke('detectAbnormalQuantities', [
        [$row, $row],
        '2026-05-11 00:00:00',
        '2026-05-11 23:59:59',
    ]);

    expect(array_column($flags, 'definition_key'))->not->toContain('multiple_identical_primary_vehicle_items');
});

it('uses attached wash certificate documents instead of safety seal text for certificate presence', function (): void {
    $row = [
        'customer_number' => 424242,
        'customer_name' => 'Flagged Customer',
        'order_id' => 9001,
        'order_item_id' => 7001,
        'product_id' => 41,
        'product_name' => 'Vaskecertifikat - Safety Seal',
        'category_name' => 'Tillæg',
        'is_wash' => 0,
        'related_item_id' => 0,
        'item_quantity' => 1,
        'max_quantity_per_order' => null,
        'safety_seal' => '',
        'has_wash_certificate_attachment' => 1,
        'order_created_at' => '2026-05-11 10:00:00',
        'reg_1' => 'AB12345',
    ];

    $flags = invoice_period_flag_service_invoke('detectAbnormalQuantities', [
        [$row],
        '2026-05-11 00:00:00',
        '2026-05-11 23:59:59',
    ]);

    expect(array_column($flags, 'definition_key'))->not->toContain('wash_certificate_item_without_certificate');
});

it('loads wash certificate attachment presence from order attachment content', function (): void {
    global $db;

    $hadDb = array_key_exists('db', $GLOBALS);
    $previousDb = $GLOBALS['db'] ?? null;
    $db = new class {
        public array $queries = [];

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

        public function query(string $sql)
        {
            $this->queries[] = $sql;

            if (str_contains($sql, 'SHOW TABLES LIKE')) {
                return $this->result([['table' => 'object_attachments']]);
            }

            if (str_contains($sql, 'FROM object_attachments')) {
                return $this->result([
                    ['object_id' => 9001, 'content' => json_encode(['other' => 'WASH_CERTIFICATE'])],
                    ['object_id' => 9002, 'content' => json_encode(['other' => 'invoice'])],
                ]);
            }

            return false;
        }

        private function result(array $rows): object
        {
            return new class($rows) {
                public int $num_rows;
                private array $rows;

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

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

    try {
        $attached = invoice_period_flag_service_invoke('getWashCertificateAttachmentOrderIds', [[9001, 9002, 9001]]);

        expect($attached)->toBe([9001 => true]);
        expect($db->queries[1])->toContain("object_type IN ('orders','`orders`')");
        expect($db->queries[1])->toContain('deleted_at IS NULL');
    } finally {
        if ($hadDb) {
            $db = $previousDb;
        } else {
            unset($GLOBALS['db']);
        }
    }
});

it('uses the highest customer-specific discount in expected price breakdowns', function (): void {
    $row = [
        'customer_number' => 0,
        'product_base_price' => 150,
        'department_price' => 100,
        'product_discount_percentage' => 5,
        'category_discount_percentage' => 12,
        'apply_category_discount' => 1,
    ];

    $expected = invoice_period_flag_service_invoke('calculateExpectedPrice', [$row]);
    $breakdown = invoice_period_flag_service_invoke('priceBreakdown', [$row, $expected]);

    expect($expected)->toBe(88);
    expect($breakdown)->toMatchArray([
        'product_price' => 150,
        'department_price' => 100,
        'effective_base_price' => 100,
        'product_discount_percentage' => 5,
        'category_discount_percentage' => 12,
        'economic_customer_discount_percentage' => 0,
        'applied_discount_percentage' => 12,
        'expected_price' => 88,
    ]);
});

it('uses a product fixed price before customer discounts in expected price breakdowns', function (): void {
    $row = [
        'customer_number' => 0,
        'product_base_price' => 1000,
        'department_price' => null,
        'product_fixed_price' => 350,
        'product_discount_percentage' => 10,
        'category_discount_percentage' => 80,
        'apply_category_discount' => 1,
    ];

    $expected = invoice_period_flag_service_invoke('calculateExpectedPrice', [$row]);
    $breakdown = invoice_period_flag_service_invoke('priceBreakdown', [$row, $expected]);

    expect($expected)->toBe(350);
    expect($breakdown)->toMatchArray([
        'product_price' => 1000,
        'effective_base_price' => 1000,
        'product_fixed_price' => 350,
        'product_discount_percentage' => 10,
        'category_discount_percentage' => 80,
        'economic_customer_discount_percentage' => 0,
        'applied_discount_percentage' => 0,
        'expected_price' => 350,
    ]);
});

it('uses a preloaded e-conomic global discount in expected price breakdowns', function (): void {
    $service = invoice_period_flag_service_instance();
    $reflection = new ReflectionClass(invoice_period_flag_service::class);

    $cache = $reflection->getProperty('economicCustomerDiscountCache');
    $cache->setValue($service, [
        35131752 => 18,
    ]);

    $calculate = $reflection->getMethod('calculateExpectedPrice');
    $breakdownMethod = $reflection->getMethod('priceBreakdown');

    $row = [
        'customer_number' => 35131752,
        'user_id' => 411,
        'product_base_price' => 100,
        'department_price' => null,
        'product_discount_percentage' => 5,
        'category_discount_percentage' => 12,
        'apply_category_discount' => 1,
    ];

    $expected = $calculate->invoke($service, $row);
    $breakdown = $breakdownMethod->invoke($service, $row, $expected);

    expect($expected)->toBe(82);
    expect($breakdown)->toMatchArray([
        'effective_base_price' => 100,
        'product_discount_percentage' => 5,
        'category_discount_percentage' => 12,
        'economic_customer_discount_percentage' => 18,
        'applied_discount_percentage' => 18,
        'expected_price' => 82,
    ]);
});

it('uses the custom-only sentinel without discounts when department price is missing', function (): void {
    $row = [
        'customer_number' => 35131752,
        'user_id' => 411,
        'product_base_price' => 100,
        'department_price' => null,
        'department_custom_pricing_only' => 1,
        'product_discount_percentage' => 50,
        'category_discount_percentage' => 25,
        'apply_category_discount' => 0,
    ];

    $expected = invoice_period_flag_service_invoke('calculateExpectedPrice', [$row]);
    $breakdown = invoice_period_flag_service_invoke('priceBreakdown', [$row, $expected]);

    expect($expected)->toBe(\objects\products_o::CUSTOM_PRICING_MISSING_PRICE);
    expect($breakdown)->toMatchArray([
        'product_price' => \objects\products_o::CUSTOM_PRICING_MISSING_PRICE,
        'department_price' => null,
        'effective_base_price' => \objects\products_o::CUSTOM_PRICING_MISSING_PRICE,
        'product_discount_percentage' => 50,
        'category_discount_percentage' => 0,
        'applied_discount_percentage' => 0,
        'expected_price' => \objects\products_o::CUSTOM_PRICING_MISSING_PRICE,
    ]);
});

it('does not report a price mismatch when a product-specific discount makes the expected price zero', function (): void {
    $row = [
        'customer_number' => 35131752,
        'customer_name' => 'BHS Logistics A/S',
        'order_id' => 61359,
        'order_item_id' => 7701,
        'invoice_collection_id' => 16891,
        'department_id' => 1,
        'product_id' => 24,
        'product_name' => 'Spot Free- Lastbil',
        'product_base_price' => 39,
        'department_price' => null,
        'product_discount_percentage' => 100,
        'category_discount_percentage' => 0,
        'apply_category_discount' => 0,
        'item_price' => 0,
        'item_quantity' => 1,
        'item_include_in_invoice' => 1,
    ];

    $expected = invoice_period_flag_service_invoke('calculateExpectedPrice', [$row]);
    $breakdown = invoice_period_flag_service_invoke('priceBreakdown', [$row, $expected]);
    $flags = invoice_period_flag_service_invoke('detectPriceMismatches', [[$row]]);

    expect($expected)->toBe(0);
    expect($breakdown)->toMatchArray([
        'product_price' => 39,
        'effective_base_price' => 39,
        'product_discount_percentage' => 100,
        'applied_discount_percentage' => 100,
        'expected_price' => 0,
    ]);
    expect($flags)->toBe([]);
});

it('does not treat subset primary vehicle product names as equivalent', function (): void {
    expect(invoice_period_flag_service_invoke('primaryVehicleProductsMatch', [5, 'Forvogn', 6, 'Forvogn med hænger']))
        ->toBeFalse()
        ->and(invoice_period_flag_service_invoke('primaryVehicleProductsMatch', [10, 'Indvendig vask Kassevogn', 11, 'Kassevogn']))
        ->toBeTrue();
});

it('rebuilds automatic invoice period data on cache misses instead of hiding warnings', function (): void {
    $content = file_get_contents(app_path('classes/invoice_period_flag_service.php'));

    expect($content)->not->toBeFalse();
    $content = (string)$content;

    expect($content)->toContain('$flags = $this->calculateAutomaticFlagsForPeriod($dateFrom, $dateTo);');
    expect($content)->toContain('$rows = $this->fetchOrderItemRowsFromDb($dateFrom, $dateTo);');
    expect($content)->toContain('cache_invoice_period_order_item_rows($dateFrom, $dateTo, $rows)');
    expect($content)->not->toContain('enqueue_invoice_period_warming($dateFrom, $dateTo)');
});

it('preloads and caches missing e-conomic discounts before price mismatch detection', function (): void {
    $content = file_get_contents(app_path('classes/invoice_period_flag_service.php'));

    expect($content)->not->toBeFalse();
    expect($content)->toContain('$this->preloadEconomicCustomerDiscounts($rows);');
    expect($content)->toContain('private function preloadEconomicCustomerDiscounts(array $rows): void');
    expect($content)->toContain("constant('redis')->get_economic_customer_discount_percentage(\$userId)");
    expect($content)->toContain('getCustomerDiscountPercentage($customerNumber)');
    expect($content)->toContain("constant('redis')->cache_economic_customer_discount_percentage(\$userId, \$discount)");
});

it('seeds order item preview cache from period rows', function (): void {
    $service = invoice_period_flag_service_instance();
    $reflection = new ReflectionClass(invoice_period_flag_service::class);

    $seed = $reflection->getMethod('seedOrderItemsPreviewCacheFromRows');
    $preview = $reflection->getMethod('getOrderItemsForPreview');

    $seed->invoke($service, [
        [
            'order_id' => 9001,
            'order_item_id' => 13,
            'product_id' => 102,
            'product_name' => 'Addon',
            'item_quantity' => 2,
            'item_price' => 25,
            'related_item_id' => 12,
        ],
        [
            'order_id' => 9001,
            'order_item_id' => 12,
            'product_id' => 101,
            'product_name' => 'Wash',
            'item_quantity' => 1,
            'item_price' => 100,
            'related_item_id' => null,
        ],
        [
            'order_id' => 9002,
            'order_item_id' => null,
        ],
    ]);

    expect($preview->invoke($service, 9001))->toBe([
        [
            'id' => 12,
            'product_id' => 101,
            'product_name' => 'Wash',
            'quantity' => 1,
            'price' => 100,
        ],
        [
            'id' => 13,
            'product_id' => 102,
            'product_name' => 'Addon',
            'quantity' => 2,
            'price' => 25,
        ],
    ])->and($preview->invoke($service, 9002))->toBe([]);
});

it('sorts manual flags before automatic warnings and preserves legacy circle indicators without flags', function (): void {
    $manual = [
        'id' => 12,
        'source' => 'manual',
        'status' => 'active',
        'created_at' => '2026-05-11 10:00:00',
    ];
    $automatic = [
        'id' => 'auto:abc',
        'source' => 'automatic',
        'status' => 'active',
        'fingerprint' => 'abc',
    ];
    $resolvedManual = [
        'id' => 13,
        'source' => 'manual',
        'status' => 'resolved',
        'created_at' => '2026-05-11 11:00:00',
    ];
    $falsePositiveAutomatic = [
        'id' => 'auto:def',
        'source' => 'automatic',
        'status' => 'false_positive',
        'fingerprint' => 'def',
    ];

    $flags = [$automatic, $manual];
    usort($flags, static fn(array $a, array $b): int => invoice_period_flag_service_invoke('sortFlags', [$a, $b]));

    expect($flags[0]['source'])->toBe('manual');
    expect(invoice_period_flag_service_invoke('countFlags', [$flags]))->toBe([
        'manual' => 1,
        'automatic' => 1,
        'total' => 2,
    ]);
    expect(invoice_period_flag_service_invoke('countFlags', [[$resolvedManual, $falsePositiveAutomatic]]))->toBe([
        'manual' => 0,
        'automatic' => 0,
        'total' => 0,
    ]);
    expect(invoice_period_flag_service_invoke('statusIndicatorForCustomer', [['requires_action' => true], [$manual]]))
        ->toBe('flag_red');
    expect(invoice_period_flag_service_invoke('statusIndicatorForCustomer', [['requires_action' => true], [$automatic]]))
        ->toBe('flag_yellow');
    expect(invoice_period_flag_service_invoke('statusIndicatorForCustomer', [
        ['requires_action' => true],
        [$resolvedManual, $falsePositiveAutomatic],
    ]))->toBe('circle_red');
    expect(invoice_period_flag_service_invoke('statusIndicatorForCustomer', [['requires_action' => true], []]))
        ->toBe('circle_red');
    expect(invoice_period_flag_service_invoke('statusIndicatorForCustomer', [['requires_action' => false], []]))
        ->toBe('circle_green');
    expect(invoice_period_flag_service_invoke('statusIndicatorForCustomer', [
        ['requires_action' => false, 'draft' => ['is_action_blocked' => true]],
        [],
    ]))->toBe('circle_yellow');
});

it('scopes invoice period flags to the customer card that can render them', function (): void {
    $customer = [
        'customer_number' => 424242,
        'transactions' => [
            ['id' => 61311, 'invoice_collection_id' => 16912],
        ],
    ];

    expect(invoice_period_flag_service_invoke('flagBelongsToCustomerCard', [
        $customer,
        ['status' => 'active', 'target_type' => 'order_item_field', 'order_id' => 61311],
        [61311 => true],
        [16912 => true],
        'all',
    ]))->toBeTrue();
    expect(invoice_period_flag_service_invoke('flagBelongsToCustomerCard', [
        $customer,
        ['status' => 'active', 'target_type' => 'order', 'order_id' => 99999],
        [61311 => true],
        [16912 => true],
        'all',
    ]))->toBeFalse();
    expect(invoice_period_flag_service_invoke('flagBelongsToCustomerCard', [
        $customer,
        ['status' => 'active', 'target_type' => 'collected_order_invoice', 'target_id' => 16912],
        [61311 => true],
        [16912 => true],
        'all',
    ]))->toBeTrue();
    expect(invoice_period_flag_service_invoke('flagBelongsToCustomerCard', [
        $customer,
        ['status' => 'active', 'target_type' => 'xlvask_usage_log', 'customer_number' => 424242],
        [61311 => true],
        [16912 => true],
        'vehicle_subscriptions',
    ]))->toBeFalse();
    expect(invoice_period_flag_service_invoke('flagBelongsToCustomerCard', [
        $customer,
        ['status' => 'active', 'target_type' => 'xlvask_usage_log', 'customer_number' => 424242],
        [61311 => true],
        [16912 => true],
        'all',
    ]))->toBeTrue();
});

it('keeps order item preview context compact for the period response', function (): void {
    global $db;

    $hadDb = array_key_exists('db', $GLOBALS);
    $previousDb = $GLOBALS['db'] ?? null;
    $db = new class {
        public string $lastQuery = '';

        public function query(string $sql): object
        {
            $this->lastQuery = $sql;
            return (object)['ok' => true];
        }

        public function fetch_all(object $result): array
        {
            return [[
                'id' => '7001',
                'order_id' => '61311',
                'product_id' => '3',
                'reference' => 'REF',
                'notes' => str_repeat('x', 1024),
                'price' => '649',
                'quantity' => '1',
                'related_item_id' => '0',
                'product_name' => 'Forvogn',
                'product_base_price' => '649',
            ]];
        }
    };

    try {
        $rows = invoice_period_flag_service_invoke('getOrderItemsForPreview', [61311]);

        expect($rows)->toBe([[
            'id' => 7001,
            'product_id' => 3,
            'product_name' => 'Forvogn',
            'quantity' => 1,
            'price' => 649,
        ]]);
        expect($db->lastQuery)->not->toContain('oi.reference');
        expect($db->lastQuery)->not->toContain('oi.notes');
        expect($db->lastQuery)->not->toContain('p.price AS product_base_price');
    } finally {
        if ($hadDb) {
            $db = $previousDb;
        } else {
            unset($GLOBALS['db']);
        }
    }
});

it('formats stored manual flags with the creating superuser display name', function (): void {
    global $db;

    $hadDb = array_key_exists('db', $GLOBALS);
    $previousDb = $GLOBALS['db'] ?? null;
    $db = new class {
        public function query(string $sql): object|false
        {
            if (str_contains($sql, 'SELECT display_name FROM users WHERE id = 42')) {
                return new class {
                    public int $num_rows = 1;

                    public function fetch_assoc(): array
                    {
                        return ['display_name' => 'Jeppe'];
                    }
                };
            }

            return false;
        }
    };

    try {
        $flag = invoice_period_flag_service_invoke('formatStoredFlag', [[
            'id' => 12,
            'source' => 'manual',
            'severity' => 'red',
            'status' => 'active',
            'target_type' => 'customer',
            'target_id' => 424242,
            'field' => null,
            'customer_number' => 424242,
            'order_id' => null,
            'order_item_id' => null,
            'invoice_collection_id' => null,
            'xlvask_usage_log_id' => null,
            'definition_key' => null,
            'fingerprint' => null,
            'reason' => 'Manual review',
            'status_reason' => null,
            'context_json' => null,
            'created_by' => 42,
            'status_changed_by' => null,
            'status_changed_at' => null,
            'created_at' => '2026-05-11 10:00:00',
            'updated_at' => '2026-05-11 10:00:00',
        ]]);

        expect($flag['created_by'])->toBe(42);
        expect($flag['created_by_name'])->toBe('Jeppe');
    } finally {
        if ($hadDb) {
            $db = $previousDb;
        } else {
            unset($GLOBALS['db']);
        }
    }
});

it('validates supported manual flag fields by target type', function (): void {
    expect(invoice_period_flag_service_invoke('normalizeField', ['order_field', 'reference']))->toBe('reference');
    expect(invoice_period_flag_service_invoke('normalizeField', ['order_item_field', 'price']))->toBe('price');
    expect(invoice_period_flag_service_invoke('normalizeField', ['customer', '']))->toBeNull();

    invoice_period_flag_service_invoke('normalizeField', ['order_field', 'price']);
})->throws(InvalidArgumentException::class, 'Invalid order flag field.');

it('wires invoice period flag routes with explicit list create and update permissions', function (): void {
    $content = file_get_contents(app_path('routes/InvoicingPeriodRoute.php'));

    expect($content)->not->toBeFalse();
    $content = (string)$content;

    expect($content)->toContain("\$this->get('/superuser/invoicing/period'");
    expect($content)->toContain("'list_invoice_period_flags' => 'List invoice period flags in the period response'");
    expect($content)->toContain("\$this->post('/superuser/invoicing/period/flags'");
    expect($content)->toContain("\$this->requirePermission('add_invoice_period_flag')");
    expect($content)->toContain("\$this->patch('/superuser/invoicing/period/flags/{id}/status'");
    expect($content)->toContain("\$this->post('/superuser/invoicing/period/flags/automatic/status'");
    expect($content)->toContain("\$this->requirePermission('update_invoice_period_flag_status')");
});

it('uses the users display_name column in detector queries', function (): void {
    $content = file_get_contents(app_path('classes/invoice_period_flag_service.php'));

    expect($content)->not->toBeFalse();
    $content = (string)$content;

    expect($content)->toContain('u.display_name AS customer_name');
    expect($content)->toContain('COALESCE(u.display_name, x.Customer) AS customer_name');
    expect($content)->not->toContain('u.name');
});

it('aggregates customer price overrides by customer number for price mismatch detection', function (): void {
    $content = file_get_contents(app_path('classes/invoice_period_flag_service.php'));

    expect($content)->not->toBeFalse();
    $content = (string)$content;

    expect($content)->toContain('MAX(po.percentage) AS percentage');
    expect($content)->toContain('GROUP BY discount_user.customer_number, po.product_or_category_id');
    expect($content)->toContain('product_discount.customer_number = o.customer_id');
    expect($content)->toContain('category_discount.customer_number = o.customer_id');
    expect($content)->not->toContain('po_product.user_id = u.id');
});

it('guards optional customer vehicle deleted_at filtering behind a column check', function (): void {
    $content = file_get_contents(app_path('classes/invoice_period_flag_service.php'));

    expect($content)->not->toBeFalse();
    $content = (string)$content;

    expect($content)->toContain("\$this->columnExists('customer_vehicles', 'deleted_at')");
    expect($content)->toContain('$deletedFilter');
    expect($content)->toContain('{$deletedFilter}');
});

it('limits historical primary product lookup to current period registrations', function (): void {
    $content = file_get_contents(app_path('classes/invoice_period_flag_service.php'));

    expect($content)->not->toBeFalse();
    $content = (string)$content;

    expect($content)->toContain('getPrimaryProductHistory($dateFrom, array_column($primaryRows, \'reg_1\'))');
    expect($content)->toContain('private function getPrimaryProductHistory(string $dateFrom, array $registrationNumbers): array');
    expect($content)->toContain('AND o.reg_1 IN ({$registrationFilter})');
    expect($content)->not->toContain('$byReg = []');
});
