const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');

const root = path.join(__dirname, '..');
const configPaths = {
    aslan: path.join(root, 'recharge', 'config', 'aslan.json'),
    yumi: path.join(root, 'recharge', 'config', 'yumi.json'),
};
const currentRuntimeTypes = {
    EG: ['FAWRY', 'VODAFONE'],
    IN: ['UPI'],
    PH: [
        'GCASH_ONLINE',
        'COINS_ONLINE',
        'INSTAPAY_QR_STATIC',
        'INSTAPAY_QR',
        'QRPH_GCASH',
        'PESONET',
        'UB_ONLINE',
        'BPI_ONLINE',
        'RCBC_ONLINE',
        'BANKVA',
        'BILL',
    ],
    PK: ['JAZZCASH'],
    VN: ['BANK_TRANSFER', 'QR'],
    TR: ['GPAY', 'BKM_EXPRESS', 'BANK_TRANSFER', 'CREDIT_CARD'],
    MX: ['SPEI', 'CODI', 'SEVEN_ELEVEN', 'OXXO', 'CREDIT_CARD'],
    BR: ['PICPAY', 'PIX', 'BOLETO', 'BANK_TRANSFER', 'CREDIT_CARD', 'BINANCE'],
};
// V5Pay 商户虽已开放 8 个国家，但各应用只有同时存在有效币种汇率和上架商品时才可展示；
// 这里锁定线上真实交集，防止前端把无法生成金额快照的空入口误放给用户。
const expectedTypesByApp = {
    aslan: Object.fromEntries(
        ['EG', 'IN', 'PH', 'PK', 'TR', 'BR'].map((code) => [
            code,
            currentRuntimeTypes[code],
        ])
    ),
    yumi: Object.fromEntries(
        ['EG', 'IN', 'PH', 'PK', 'VN', 'TR', 'BR'].map((code) => [
            code,
            currentRuntimeTypes[code],
        ])
    ),
};

function response(payload) {
    return {
        headers: { get: () => '' },
        ok: true,
        status: 200,
        statusText: 'OK',
        text: () =>
            Promise.resolve(
                JSON.stringify({ code: 'OK', message: 'ok', data: payload })
            ),
    };
}

function createAPIHarness() {
    const requests = [];
    const storage = new Map();
    let nextPayload = {};
    const window = {
        document: { documentElement: { setAttribute: () => {} } },
        fetch: (rawURL, options = {}) => {
            // 每次调用都记录真实序列化后的 URL/body，测试的是浏览器最终发出的契约，而不是内部 helper 的中间对象。
            requests.push({
                url: new URL(rawURL),
                options,
                body: options.body ? JSON.parse(options.body) : null,
            });
            return Promise.resolve(response(nextPayload));
        },
        localStorage: {
            getItem: (key) => storage.get(key) || null,
            removeItem: (key) => storage.delete(key),
            setItem: (key, value) => storage.set(key, String(value)),
        },
        location: {
            hash: '',
            hostname: 'h5.global-interaction.com',
            href: 'https://h5.global-interaction.com/recharge/index.html',
            origin: 'https://h5.global-interaction.com',
            pathname: '/recharge/index.html',
            search: '?language=tr',
        },
    };
    const context = vm.createContext({
        FormData,
        JSON,
        Math,
        Number,
        Object,
        Promise,
        String,
        URL,
        URLSearchParams,
        Uint32Array,
        window,
    });
    vm.runInContext(
        fs.readFileSync(path.join(root, 'common', 'api.js'), 'utf8'),
        context,
        { filename: 'common/api.js' }
    );
    return {
        api: window.HyAppAPI,
        requests,
        respondWith(payload) {
            nextPayload = payload;
        },
    };
}

function readConfigs() {
    return Object.fromEntries(
        Object.entries(configPaths).map(([app, file]) => [
            app,
            JSON.parse(fs.readFileSync(file, 'utf8')),
        ])
    );
}

function assertRuntimeConfig(config, app) {
    const provider = config.providers && config.providers.v5pay;
    assert.ok(provider, `${app} should configure V5Pay`);
    assert.equal(provider.provider_code, 'v5pay');
    assert.equal(provider.provider_name, 'V5Pay');
    assert.equal(provider.status, 'active');
    assert.deepEqual(
        Object.fromEntries(
            provider.countries.map((country) => [
                country.country_code,
                country.methods.map((method) => method.pay_type),
            ])
        ),
        expectedTypesByApp[app],
        `${app} should expose only V5Pay countries backed by current app products`
    );
    provider.countries.forEach((country) => {
        country.methods.forEach((method) => {
            assert.equal(method.channel_code, method.pay_type);
            assert.equal(method.logo_url, '');
            assert.ok(method.pay_way, `${country.country_code} needs pay_way`);
            assert.ok(method.method_name, `${method.pay_type} needs a name`);
            // Lalu 的 method_id 和商户密钥属于后端数据，复制到 app JSON 会造成跨应用错误绑定或敏感信息泄露。
            assert.equal(Object.hasOwn(method, 'method_id'), false);
            assert.equal(Object.hasOwn(method, 'secret_key'), false);
        });
    });
}

async function assertProviderParsing(harness, config, app) {
    harness.api.setAppConfig(config);
    const currencies = Object.fromEntries(
        config.providers.v5pay.countries.map((country) => [
            country.country_code,
            country.currency_code,
        ])
    );

    for (const [countryCode, types] of Object.entries(
        expectedTypesByApp[app]
    )) {
        if (app === 'aslan') {
            harness.respondWith({
                country_code: countryCode,
                currency_code: currencies[countryCode],
                products: [{ product_id: 'legacy-product' }],
            });
        } else {
            harness.respondWith({
                countryCode,
                currencyCode: currencies[countryCode],
                payCountryId: 'pay-country-1',
                commodity: [
                    {
                        id: 'legacy-product',
                        amountUsd: 1,
                        amount: 1,
                        currency: currencies[countryCode],
                    },
                ],
                // 生产旧接口当前只返回 MiFaPay 数据库渠道。V5Pay 使用下单时的安全合成快照，
                // 必须证明它不会被一个完全无关的旧渠道集合误过滤掉。
                channels: [
                    {
                        channel: { channelCode: 'MIFA_ONLY_CHANNEL' },
                        details: { factoryChannel: 'MIFA_ONLY_TYPE' },
                    },
                ],
            });
        }
        const options = await harness.api.recharge.options('100000001', {
            country_code: countryCode,
            currency_code: currencies[countryCode],
            pay_country_id: 'pay-country-1',
            user_id: '100000001',
        });
        const v5Methods = Array.from(options.payment_methods).filter(
            (method) => method.provider_code === 'v5pay'
        );
        const v5Types = v5Methods.map((method) => method.pay_type);
        assert.deepEqual(
            v5Types,
            types,
            `${app} should parse ${countryCode} V5Pay methods`
        );
        v5Methods.forEach((method) => {
            assert.equal(method.provider_name, 'V5Pay');
            assert.equal(method.channel_code, method.pay_type);
            assert.match(method.method_id, /^v5pay:/);
        });
    }
}

async function assertLegacyOrderPayloads(harness, configs) {
    const legacyProductID = '90071992547409931234';
    for (const app of ['aslan', 'yumi']) {
        harness.api.setAppConfig(configs[app]);
        harness.respondWith({
            orderId: `${app}-order-1`,
            factoryCode: app === 'yumi' ? 'V5_PAY' : undefined,
            providerCode: app === 'aslan' ? 'v5pay' : undefined,
            requestUrl: `https://pay.example/${app}-order-1`,
        });
        const order = await harness.api.recharge.createOrder({
            product_id: legacyProductID,
            pay_country_id: 'pay-country-1',
            user_id: '100000001',
            provider_code: 'v5pay',
            pay_way: 'Cash',
            pay_type: 'FAWRY',
            channel_code: 'FAWRY',
            country_code: 'EG',
            currency_code: 'EGP',
            command_id: `${app}-command-1`,
            return_url: 'https://h5.global-interaction.com/recharge/index.html',
        });
        const body = harness.requests.at(-1).body;
        assert.equal(body.goodsId, legacyProductID);
        assert.equal(typeof body.goodsId, 'string');
        assert.equal(body.providerCode, 'v5pay');
        assert.equal(body.payWay, 'Cash');
        assert.equal(body.payType, 'FAWRY');
        assert.equal(body.channelCode, 'FAWRY');
        if (app === 'yumi') {
            assert.equal(body.countryCode, 'EG');
            assert.equal(body.currencyCode, 'EGP');
            assert.equal(body.commandId, 'yumi-command-1');
            assert.equal(body.language, 'tr');
            assert.equal(order.provider_code, 'v5pay');

            harness.respondWith({
                orderId: 'yumi-order-1',
                factoryCode: 'V5PAY',
                status: 'SUCCESS',
                amount: '2.00',
            });
            const status = await harness.api.recharge.getOrder(
                'yumi-order-1',
                '100000001'
            );
            assert.equal(status.provider_code, 'v5pay');
            assert.equal(status.status, 'credited');
        }
    }
}

async function assertLaluNumericPayload(harness) {
    harness.api.setAppConfig(null);
    harness.respondWith({ order_id: 'lalu-order-1' });
    await harness.api.recharge.createOrder({
        product_id: 123456,
        provider_code: 'v5pay',
    });
    const body = harness.requests.at(-1).body;
    assert.equal(body.product_id, 123456);
    assert.equal(typeof body.product_id, 'number');
}

function extractPageFunction(source, name) {
    const start = source.indexOf(`function ${name}(`);
    assert.notEqual(start, -1, `missing recharge page function ${name}`);
    const next = source.indexOf('\n                function ', start + 1);
    return source.slice(start, next === -1 ? source.length : next);
}

function assertPageProductIDCompatibility() {
    const pageSource = fs.readFileSync(
        path.join(root, 'recharge', 'index.html'),
        'utf8'
    );
    const source = [
        extractPageFunction(pageSource, 'productID'),
        extractPageFunction(pageSource, 'createOrderProductID'),
        extractPageFunction(pageSource, 'isLegacyWebPayApp'),
    ].join('\n');
    const context = vm.createContext({ isFinite, Number, String, state: {} });
    vm.runInContext(source, context, { filename: 'recharge/index.html' });

    context.state.appConfig = { adapter: 'yumi-web-pay' };
    assert.equal(
        context.createOrderProductID({ product_id: '90071992547409931234' }),
        '90071992547409931234'
    );
    context.state.appConfig = { adapter: 'aslan-web-pay' };
    assert.equal(
        context.createOrderProductID({ product_id: '123456' }),
        '123456'
    );
    context.state.appConfig = null;
    assert.equal(
        context.createOrderProductID({ product_id: '123456' }),
        123456
    );
}

function assertPageProviderCountries(configs) {
    const pageSource = fs.readFileSync(
        path.join(root, 'recharge', 'index.html'),
        'utf8'
    );
    const source = extractPageFunction(
        pageSource,
        'configuredProviderCountries'
    );
    const context = vm.createContext({ Object, Array, state: {} });
    vm.runInContext(source, context, { filename: 'recharge/index.html' });
    Object.entries(configs).forEach(([app, config]) => {
        context.state.appConfig = config;
        const codes = Array.from(context.configuredProviderCountries()).map(
            (country) => country.country_code
        );
        Object.keys(expectedTypesByApp[app]).forEach((countryCode) => {
            assert.ok(
                codes.includes(countryCode),
                `${app} page country selection should include ${countryCode}`
            );
        });
    });
}

async function main() {
    const configs = readConfigs();
    Object.entries(configs).forEach(([app, config]) =>
        assertRuntimeConfig(config, app)
    );
    const harness = createAPIHarness();
    await assertProviderParsing(harness, configs.aslan, 'aslan');
    await assertProviderParsing(harness, configs.yumi, 'yumi');
    await assertLegacyOrderPayloads(harness, configs);
    await assertLaluNumericPayload(harness);
    assertPageProductIDCompatibility();
    assertPageProviderCountries(configs);
    console.log('recharge V5Pay compatibility tests passed');
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});
