/**
 * Deterministic checks for margin-analytics (Phase 4).
 * Import and call `runMarginAnalyticsFixtureChecks()` from devtools or a test runner.
 */

import {
    buildSkuAverageUnitCostMap,
    computeOrderDerivedMargin,
    computeShrinkageCostEstimate,
    computeMarginByDateLastDays,
    buildSkuLedgerSaleSnapshotCostMap,
    resolveUnitCostPhase5,
    computeOrderDerivedMarginFiltered,
    computeShrinkageCostEstimateFiltered
} from './margin-analytics.js';
import {
    dateInputToStartMs,
    dateInputToEndMs,
    normalizeAnalyticsDateRange,
    processSalesByDateInRange,
    buildPrioritizedActionQueue,
    computeDayCloseReconciliation,
    buildRestockHintRows
} from './finance-analytics-v5.js';
import { roundMoney2, computeExpectedTenderByMethod } from './pos-finance.js';
import { getOrderCreatedAtMs } from './order-time-channel.js';

function assert(cond, msg, errors) {
    if (!cond) errors.push(msg);
}

/** @returns {{ ok: boolean, errors: string[] }} */
export function runMarginAnalyticsFixtureChecks() {
    const errors = [];
    const purchases = {
        batch1: {
            items: [{ productId: 'skuA', variantId: null, qty: 10, unitBuyPrice: 50, lineDiscount: 0 }]
        }
    };
    const ledger = {};
    const { avgMap } = buildSkuAverageUnitCostMap(purchases, ledger);
    assert(avgMap.has('skuA|'), 'avg map should contain skuA|', errors);
    assert(Math.abs((avgMap.get('skuA|')?.avgUnitCost || 0) - 50) < 0.02, 'avg unit cost should be 50', errors);

    const products = {
        skuA: { id: 'skuA', name: 'A', salePrice: 120, isWeighted: false, stock: 0 }
    };
    const now = Date.now();
    const orders = [
        {
            channel: 'pos',
            createdAt: now,
            totalBill: 200,
            items: [{ id: 'skuA', name: 'A', quantity: 2, price: 100, refundedQty: 0 }]
        },
        {
            channel: 'online',
            createdAt: now - 86400000,
            totalBill: 100,
            items: [{ id: 'skuA', name: 'A', quantity: 2, price: 100, refundedQty: 1 }]
        }
    ];
    const m = computeOrderDerivedMargin(orders, products, avgMap, 'all');
    // Order1: net 2 * 100 rev, cogs 2*50=100. Order2: net 1 * 100 rev, cogs 50
    const expRev = roundMoney2(200 + 100);
    const expCogs = roundMoney2(100 + 50);
    assert(Math.abs(m.revenue - expRev) < 0.02, `revenue expected ${expRev} got ${m.revenue}`, errors);
    assert(Math.abs(m.cogs - expCogs) < 0.02, `cogs expected ${expCogs} got ${m.cogs}`, errors);
    assert(Math.abs(m.grossProfit - roundMoney2(expRev - expCogs)) < 0.02, 'gross profit', errors);

    const shrinkLedger = {
        w1: {
            eventType: 'wastage',
            productId: 'skuA',
            variantId: null,
            qtyDelta: -2,
            unitCost: 50,
            createdAt: now
        }
    };
    const shrink = computeShrinkageCostEstimate(shrinkLedger, products, avgMap);
    assert(Math.abs(shrink - 100) < 0.02, `shrinkage expected 100 got ${shrink}`, errors);

    const byDay = computeMarginByDateLastDays(orders, avgMap, products, 'all', 7);
    const dayKey = new Date(now).toISOString().split('T')[0];
    const cell = byDay.get(dayKey);
    assert(cell && cell.revenue > 0, 'margin-by-date should include today', errors);

    const snap = buildSkuLedgerSaleSnapshotCostMap(
        {
            le1: {
                eventType: 'sale',
                productId: 'skuA',
                variantId: null,
                qtyDelta: -1,
                unitCost: 40,
                createdAt: now
            }
        },
        now - 1000,
        now + 1000
    );
    const r5 = resolveUnitCostPhase5(avgMap, snap, 'skuA', null, products.skuA);
    assert(Math.abs(r5.unitCost - 40) < 0.02, 'ledger snapshot should override avg 50', errors);

    const mF = computeOrderDerivedMarginFiltered(orders, products, avgMap, snap, 'all', {
        startMs: now - 86400000 * 2,
        endMs: now + 86400000,
        categoryId: null,
        weightedOnly: false
    });
    assert(mF.revenue > 0, 'filtered margin should have revenue', errors);

    const shrinkF = computeShrinkageCostEstimateFiltered(shrinkLedger, products, avgMap, now - 1000, now + 86400000);
    assert(Math.abs(shrinkF - 100) < 0.02, 'filtered shrinkage', errors);

    const dayStart = dateInputToStartMs(new Date(now).toISOString().split('T')[0]);
    const dayEnd = dateInputToEndMs(new Date(now).toISOString().split('T')[0]);
    const recon = computeDayCloseReconciliation(orders, products, purchases, shrinkLedger, dayStart, dayEnd, 'all');
    assert(recon.orderCount >= 1, 'recon should see orders', errors);

    return { ok: errors.length === 0, errors };
}

/**
 * Phase 5 visibility / stability checks (date guards, sales map, action queue).
 * @returns {{ ok: boolean, errors: string[] }}
 */
export function runPhase5FixtureChecks() {
    const base = runMarginAnalyticsFixtureChecks();
    const errors = [...base.errors];

    const emptyRange = normalizeAnalyticsDateRange('', '');
    assert(
        emptyRange.startMs != null && emptyRange.endMs != null && emptyRange.startMs <= emptyRange.endMs,
        'normalizeAnalyticsDateRange empty inputs should yield ordered ms',
        errors
    );

    const inverted = normalizeAnalyticsDateRange('2025-01-10', '2025-01-01');
    assert(
        inverted.startMs <= inverted.endMs && inverted.fromStr <= inverted.toStr,
        'normalizeAnalyticsDateRange should swap inverted inputs',
        errors
    );

    const partial = normalizeAnalyticsDateRange('2025-06-01', '');
    assert(
        partial.toStr === '2025-06-01' && partial.startMs != null && partial.endMs != null,
        'normalizeAnalyticsDateRange should mirror single bound when one side missing',
        errors
    );

    const salesNull = processSalesByDateInRange([], 'all', null, null);
    assert(salesNull.size === 0, 'processSalesByDateInRange(null range) should be empty', errors);

    const qEmpty = buildPrioritizedActionQueue({
        belowCostRows: [],
        lowMarginRows: [],
        shrinkRows: [],
        reconVariancePct: 2,
        billTotal: 0,
        varianceBillVsLine: 0
    });
    assert(Array.isArray(qEmpty) && qEmpty.length === 0, 'empty action queue context yields no items', errors);

    const nowT = Date.now();
    const ordMap = {
        a: { channel: 'pos', status: 'Delivered', totalBill: 100, createdAt: nowT, payment: { breakdown: [{ method: 'Cash', amount: 100 }] } }
    };
    const tend = computeExpectedTenderByMethod(ordMap, nowT - 1000, nowT + 1000, (o) => o.channel === 'pos', (o) => getOrderCreatedAtMs(o));
    assert(Math.abs(tend.cash - 100) < 0.02, 'POS tender cash should match', errors);

    const rest = buildRestockHintRows(
        [{ channel: 'pos', createdAt: nowT, items: [{ id: 'skuZ', name: 'Z', quantity: 10, price: 5, refundedQty: 0 }], totalBill: 50 }],
        { skuZ: { id: 'skuZ', name: 'Z', category: 'x', isWeighted: false, stock: 3 } },
        'pos',
        { startMs: nowT - 86400000, endMs: nowT + 1000, categoryId: null, weightedOnly: false }
    );
    assert(Array.isArray(rest) && rest.length >= 1, 'restock hints should return rows for fast+low stock', errors);

    return { ok: errors.length === 0, errors };
}
