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

const knex = require('knex').default;

const importers = require('../../../../../core/server/data/seeders/importers');
const ProductsImporter = importers.find(i => i.table === 'products');
const OfferRedemptionsImporter = importers.find(i => i.table === 'offer_redemptions');
const StripeProductsImporter = importers.find(i => i.table === 'stripe_products');
const StripePricesImporter = importers.find(i => i.table === 'stripe_prices');

const generateEvents = require('../../../../../core/server/data/seeders/utils/event-generator');
const dateToDatabaseString = require('../../../../../core/server/data/seeders/utils/database-date');

const DataGenerator = require('../../../../../core/server/data/seeders/data-generator');

const schema = require('../../../../../core/server/data/schema');

const schemaTables = schema.tables;

describe('Data Generator', function () {
    let db;

    beforeEach(async function () {
        db = knex({
            client: 'better-sqlite3',
            useNullAsDefault: true,
            connection: {
                filename: ':memory:'
            }
        });

        for (const tableName of Object.keys(schema.tables)) {
            await db.schema.createTable(tableName, function (table) {
                for (const rowName of Object.keys(schema.tables[tableName])) {
                    const row = schema.tables[tableName][rowName];

                    if (rowName === '@@UNIQUE_CONSTRAINTS@@') {
                        for (const constraints of row) {
                            table.unique(constraints);
                        }
                        break;
                    } else if (rowName === '@@INDEXES@@') {
                        for (const indexes of row) {
                            // We ignore the index prefix for SQLite.
                            if (indexes && typeof indexes === 'object' && !Array.isArray(indexes)) {
                                table.index(indexes.columns);
                            } else {
                                table.index(indexes);
                            }
                        }
                        break;
                    } else if (rowName === '@@PRIMARY_KEY@@') {
                        table.primary(row);
                        break;
                    }

                    let rowChain = table[row.type.toLowerCase()](rowName);
                    if ('nullable' in row) {
                        if (row.nullable) {
                            rowChain = rowChain.nullable();
                        } else {
                            rowChain = rowChain.notNullable();
                        }
                    }
                    if ('defaultTo' in row) {
                        rowChain = rowChain.defaultTo(row.defaultTo);
                    }
                    if ('references' in row) {
                        const [foreignTable, foreignRow] = row.references.split('.');
                        rowChain = rowChain.references(foreignRow).inTable(foreignTable);
                    }
                    if (row.unique) {
                        table.unique([rowName]);
                    }
                    if (row.primary) {
                        table.primary(rowName);
                    }
                }
            });
        }
    });

    afterEach(async function () {
        await db.destroy();
    });

    it('Can import the whole dataset without error', async function () {
        const dataGenerator = new DataGenerator({
            knex: db,
            schema,
            schemaTables,
            logger: {
                info: () => { },
                ok: () => { },
                warn: () => { }
            },
            tables: [{
                name: 'members',
                quantity: 10
            }, {
                name: 'posts',
                quantity: 2
            }],
            withDefault: true
        });
        await dataGenerator.importData();
    });

    it('Can import explicit offer redemptions', async function () {
        const dataGenerator = new DataGenerator({
            knex: db,
            schema,
            schemaTables,
            logger: {
                info: () => {},
                ok: () => {},
                warn: () => {}
            },
            tables: [{
                name: 'offers',
                quantity: 12
            }, {
                name: 'offer_redemptions',
                quantity: 10
            }],
            quantities: {
                members: 100,
                members_stripe_customers: 100,
                members_stripe_customers_subscriptions: 100
            }
        });

        await dataGenerator.importData();

        const offers = await db.select('name', 'code').from('offers');
        const redemptions = await db.select('offer_id', 'subscription_id').from('offer_redemptions');

        assert.equal(offers.length, 12);
        assert.equal(new Set(offers.map(offer => offer.name)).size, 12);
        assert.equal(new Set(offers.map(offer => offer.code)).size, 12);
        assert.equal(redemptions.length, 10);
        assert.equal(new Set(redemptions.map(redemption => `${redemption.subscription_id}:${redemption.offer_id}`)).size, 10);
    });
});

describe('Importer', function () {
    let db;

    beforeEach(async function () {
        db = knex({
            client: 'better-sqlite3',
            useNullAsDefault: true,
            connection: {
                filename: ':memory:'
            }
        });

        await db.schema.createTable('products', function (table) {
            table.string('id');
            table.string('name');
            table.string('slug');
            table.string('visibility');
            table.date('created_at');
            table.string('type');
            table.string('description');
            table.string('currency');
            table.integer('monthly_price');
            table.integer('yearly_price');
            table.string('monthly_price_id');
            table.string('yearly_price_id');
        });

        await db.schema.createTable('stripe_products', function (table) {
            table.string('id');
            table.string('product_id');
            table.string('stripe_product_id');
            table.date('created_at');
            table.date('updated_at');
        });

        await db.schema.createTable('stripe_prices', function (table) {
            table.string('id');
            table.string('stripe_price_id');
            table.string('stripe_product_id');
            table.boolean('active');
            table.string('nickname');
            table.string('currency');
            table.integer('amount');
            table.string('type');
            table.string('interval');
            table.string('description');
            table.date('created_at');
            table.date('updated_at');
        });
    });

    afterEach(async function () {
        await db.destroy();
    });

    it('Should import a single item', async function () {
        const transaction = await db.transaction();
        const productsImporter = new ProductsImporter(db, transaction);
        await productsImporter.import();
        transaction.commit();

        const products = await db.select('id', 'name').from('products');

        assert.equal(products.length, 4);
        assert.equal(products[0].name, 'Free');
    });

    it('Should import an item for each entry in an array', async function () {
        const transaction = await db.transaction();
        const productsImporter = new ProductsImporter(db, transaction);
        await productsImporter.import();

        const stripeProductsImporter = new StripeProductsImporter(db, transaction);
        await stripeProductsImporter.import();
        transaction.commit();

        const results = await db.select('id').from('stripe_products');

        assert.equal(results.length, 4);
    });

    it('Should update products to reference price ids', async function () {
        const transaction = await db.transaction();
        const productsImporter = new ProductsImporter(db, transaction);
        await productsImporter.import();

        const stripeProductsImporter = new StripeProductsImporter(db, transaction);
        await stripeProductsImporter.import();

        const stripePricesImporter = new StripePricesImporter(db, transaction);
        await stripePricesImporter.import();

        await productsImporter.finalise();
        await stripeProductsImporter.finalise();
        await stripePricesImporter.finalise();
        transaction.commit();

        const results = await db.select('id', 'name', 'monthly_price_id', 'yearly_price_id').from('products');

        assert.equal(results.length, 4);
        assert.equal(results[0].name, 'Free');
    });

    it('Clamps redemption timestamps to the subscription window', function () {
        const importer = new OfferRedemptionsImporter(db, null);
        const subscriptionState = {
            subscriptionCreatedAt: new Date('2024-01-01T00:00:00.000Z'),
            redemptionEndAt: new Date('2024-01-01T00:00:01.000Z'),
            lastRedeemedAt: new Date('2024-01-01T00:00:00.500Z')
        };
        const offer = {
            created_at: '2024-01-01T00:00:00.000Z'
        };

        importer.getCreatedAt(subscriptionState, offer);

        assert.equal(subscriptionState.lastRedeemedAt.valueOf(), subscriptionState.redemptionEndAt.valueOf());
    });
});

describe('Events Generator', function () {
    it('Parses database timestamps as UTC in non-UTC timezones', function () {
        const originalTZ = process.env.TZ;
        try {
            process.env.TZ = 'America/New_York';
            const result = dateToDatabaseString.parse('2026-03-26 11:50:00.000');
            assert.equal(result.toISOString(), '2026-03-26T11:50:00.000Z');
        } finally {
            if (originalTZ === undefined) {
                delete process.env.TZ;
            } else {
                process.env.TZ = originalTZ;
            }
        }
    });

    it('Returns the start date when a range is inverted', function () {
        const startDate = new Date('2026-03-26T11:50:00.000Z');
        const endDate = new Date('2026-03-26T10:00:00.000Z');

        assert.equal(dateToDatabaseString.randomBetween(startDate, endDate).toISOString(), startDate.toISOString());
    });

    it('Generates a set of timestamps which meet the criteria', function () {
        const startTime = new Date();
        startTime.setDate(startTime.getDate() - 30);
        const endTime = new Date();
        const timestamps = generateEvents({
            shape: 'flat',
            total: 100,
            trend: 'positive',
            startTime,
            endTime
        });

        for (const timestamp of timestamps) {
            assert(timestamp.valueOf() <= endTime.valueOf());
            assert(timestamp.valueOf() >= startTime.valueOf());
        }
    });

    it('Works for a set of shapes', function () {
        const startTime = new Date();
        startTime.setDate(startTime.getDate() - 30);
        const endTime = new Date();

        const options = {
            startTime,
            endTime,
            total: 100,
            trend: 'positive'
        };

        const shapes = ['linear', 'flat', 'ease-in', 'ease-out'];

        for (const shape of shapes) {
            generateEvents(Object.assign({}, options, {shape}));
        }
    });
});
