const {
    safeAddColumns,
    safeCreateTable,
    safeAddIndex,
    safeRemoveColumn,
    safeChangeColumn,
} = require('../../../utils/migration-utils');

describe('migration-utils', () => {
    let queryInterface;

    beforeEach(() => {
        queryInterface = {
            showAllTables: jest.fn(),
            describeTable: jest.fn(),
            addColumn: jest.fn(),
            removeColumn: jest.fn(),
            createTable: jest.fn(),
            showIndex: jest.fn(),
            addIndex: jest.fn(),
            changeColumn: jest.fn(),
            sequelize: {
                getDialect: jest.fn().mockReturnValue('postgres'),
                query: jest.fn(),
            },
        };
    });

    describe('safeAddColumns', () => {
        it('should skip when table does not exist', async () => {
            queryInterface.showAllTables.mockResolvedValue(['other_table']);
            await safeAddColumns(queryInterface, 'missing_table', [
                { name: 'col1', definition: { type: 'TEXT' } },
            ]);
            expect(queryInterface.addColumn).not.toHaveBeenCalled();
        });

        it('should add column when it does not exist', async () => {
            queryInterface.showAllTables.mockResolvedValue(['tasks']);
            queryInterface.describeTable.mockResolvedValue({
                id: {},
                title: {},
            });
            const definition = { type: 'TEXT' };

            await safeAddColumns(queryInterface, 'tasks', [
                { name: 'description', definition },
            ]);

            expect(queryInterface.addColumn).toHaveBeenCalledWith(
                'tasks',
                'description',
                definition
            );
        });

        it('should skip column when it already exists', async () => {
            queryInterface.showAllTables.mockResolvedValue(['tasks']);
            queryInterface.describeTable.mockResolvedValue({
                id: {},
                title: {},
            });

            await safeAddColumns(queryInterface, 'tasks', [
                { name: 'title', definition: { type: 'TEXT' } },
            ]);

            expect(queryInterface.addColumn).not.toHaveBeenCalled();
        });

        it('should add multiple columns, skipping existing ones', async () => {
            queryInterface.showAllTables.mockResolvedValue(['tasks']);
            queryInterface.describeTable.mockResolvedValue({
                id: {},
                title: {},
            });

            await safeAddColumns(queryInterface, 'tasks', [
                { name: 'title', definition: { type: 'TEXT' } }, // exists
                { name: 'priority', definition: { type: 'INTEGER' } }, // new
                { name: 'due_date', definition: { type: 'DATE' } }, // new
            ]);

            expect(queryInterface.addColumn).toHaveBeenCalledTimes(2);
            expect(queryInterface.addColumn).toHaveBeenCalledWith(
                'tasks',
                'priority',
                { type: 'INTEGER' }
            );
            expect(queryInterface.addColumn).toHaveBeenCalledWith(
                'tasks',
                'due_date',
                { type: 'DATE' }
            );
        });

        it('should re-throw errors from addColumn', async () => {
            queryInterface.showAllTables.mockResolvedValue(['tasks']);
            queryInterface.describeTable.mockResolvedValue({ id: {} });
            queryInterface.addColumn.mockRejectedValue(new Error('DB error'));

            await expect(
                safeAddColumns(queryInterface, 'tasks', [
                    { name: 'col', definition: { type: 'TEXT' } },
                ])
            ).rejects.toThrow('DB error');
        });
    });

    describe('safeCreateTable', () => {
        it('should create table when it does not exist', async () => {
            queryInterface.showAllTables.mockResolvedValue([]);
            const definition = { id: { type: 'INTEGER', primaryKey: true } };

            await safeCreateTable(queryInterface, 'new_table', definition);

            expect(queryInterface.createTable).toHaveBeenCalledWith(
                'new_table',
                definition
            );
        });

        it('should skip when table already exists', async () => {
            queryInterface.showAllTables.mockResolvedValue(['existing_table']);

            await safeCreateTable(queryInterface, 'existing_table', {});

            expect(queryInterface.createTable).not.toHaveBeenCalled();
        });

        it('should re-throw errors from createTable', async () => {
            queryInterface.showAllTables.mockResolvedValue([]);
            queryInterface.createTable.mockRejectedValue(
                new Error('Create failed')
            );

            await expect(
                safeCreateTable(queryInterface, 'fail_table', {})
            ).rejects.toThrow('Create failed');
        });
    });

    describe('safeAddIndex', () => {
        it('should skip when table does not exist', async () => {
            queryInterface.showAllTables.mockResolvedValue([]);
            await safeAddIndex(queryInterface, 'missing_table', ['col1']);
            expect(queryInterface.addIndex).not.toHaveBeenCalled();
        });

        it('should add index when it does not exist', async () => {
            queryInterface.showAllTables.mockResolvedValue(['tasks']);
            queryInterface.showIndex.mockResolvedValue([]);

            await safeAddIndex(queryInterface, 'tasks', ['user_id'], {
                name: 'idx_user',
            });

            expect(queryInterface.addIndex).toHaveBeenCalledWith(
                'tasks',
                ['user_id'],
                { name: 'idx_user' }
            );
        });

        it('should skip when a matching index already exists', async () => {
            queryInterface.showAllTables.mockResolvedValue(['tasks']);
            queryInterface.showIndex.mockResolvedValue([
                { fields: [{ attribute: 'user_id' }] },
            ]);

            await safeAddIndex(queryInterface, 'tasks', ['user_id']);

            expect(queryInterface.addIndex).not.toHaveBeenCalled();
        });

        it('should not throw on addIndex failure (swallows error)', async () => {
            queryInterface.showAllTables.mockResolvedValue(['tasks']);
            queryInterface.showIndex.mockResolvedValue([]);
            queryInterface.addIndex.mockRejectedValue(new Error('Index error'));

            // safeAddIndex swallows errors (no throw in the catch)
            await expect(
                safeAddIndex(queryInterface, 'tasks', ['col'])
            ).resolves.toBeUndefined();
        });
    });

    describe('safeRemoveColumn', () => {
        it('should skip when column does not exist', async () => {
            queryInterface.describeTable.mockResolvedValue({
                id: {},
                title: {},
            });

            await safeRemoveColumn(queryInterface, 'tasks', 'nonexistent');

            expect(queryInterface.removeColumn).not.toHaveBeenCalled();
        });

        it('should remove column on non-SQLite dialect', async () => {
            queryInterface.describeTable.mockResolvedValue({
                id: {},
                title: {},
                old_col: {},
            });
            queryInterface.sequelize.getDialect.mockReturnValue('postgres');

            await safeRemoveColumn(queryInterface, 'tasks', 'old_col');

            expect(queryInterface.removeColumn).toHaveBeenCalledWith(
                'tasks',
                'old_col'
            );
        });

        it('should use table recreation strategy on SQLite', async () => {
            queryInterface.describeTable.mockResolvedValue({
                id: {
                    type: 'INTEGER',
                    primaryKey: true,
                    autoIncrement: true,
                    allowNull: false,
                },
                title: { type: 'TEXT', allowNull: true },
                remove_me: { type: 'TEXT', allowNull: true },
            });
            queryInterface.sequelize.getDialect.mockReturnValue('sqlite');

            await safeRemoveColumn(queryInterface, 'tasks', 'remove_me');

            // Should have called multiple queries for SQLite recreation
            const calls = queryInterface.sequelize.query.mock.calls.map(
                (c) => c[0]
            );
            expect(
                calls.some((q) => q.includes('PRAGMA foreign_keys = OFF'))
            ).toBe(true);
            expect(
                calls.some((q) => q.includes('CREATE TABLE tasks_new'))
            ).toBe(true);
            expect(calls.some((q) => q.includes('INSERT INTO tasks_new'))).toBe(
                true
            );
            expect(calls.some((q) => q.includes('DROP TABLE tasks'))).toBe(
                true
            );
            expect(
                calls.some((q) =>
                    q.includes('ALTER TABLE tasks_new RENAME TO tasks')
                )
            ).toBe(true);
            expect(
                calls.some((q) => q.includes('PRAGMA foreign_keys = ON'))
            ).toBe(true);
            // remove_me should not appear in CREATE TABLE
            const createCall = calls.find((q) =>
                q.includes('CREATE TABLE tasks_new')
            );
            expect(createCall).not.toContain('remove_me');
        });

        it('should re-throw errors', async () => {
            queryInterface.describeTable.mockRejectedValue(
                new Error('Describe failed')
            );

            await expect(
                safeRemoveColumn(queryInterface, 'tasks', 'col')
            ).rejects.toThrow('Describe failed');
        });
    });

    describe('safeChangeColumn', () => {
        it('should skip when table does not exist', async () => {
            queryInterface.showAllTables.mockResolvedValue([]);

            await safeChangeColumn(queryInterface, 'missing', 'col', {
                type: 'TEXT',
            });

            expect(queryInterface.changeColumn).not.toHaveBeenCalled();
        });

        it('should skip when column does not exist', async () => {
            queryInterface.showAllTables.mockResolvedValue(['tasks']);
            queryInterface.describeTable.mockResolvedValue({ id: {} });

            await safeChangeColumn(queryInterface, 'tasks', 'nonexistent', {
                type: 'TEXT',
            });

            expect(queryInterface.changeColumn).not.toHaveBeenCalled();
        });

        it('should change column on non-SQLite dialect', async () => {
            queryInterface.showAllTables.mockResolvedValue(['tasks']);
            queryInterface.describeTable.mockResolvedValue({
                id: {},
                title: { type: 'TEXT', allowNull: true },
            });
            queryInterface.sequelize.getDialect.mockReturnValue('postgres');
            const newDef = { type: 'VARCHAR(255)', allowNull: false };

            await safeChangeColumn(queryInterface, 'tasks', 'title', newDef);

            expect(queryInterface.changeColumn).toHaveBeenCalledWith(
                'tasks',
                'title',
                newDef
            );
        });

        it('should use table recreation on SQLite', async () => {
            queryInterface.showAllTables.mockResolvedValue(['tasks']);
            queryInterface.describeTable.mockResolvedValue({
                id: {
                    type: 'INTEGER',
                    primaryKey: true,
                    autoIncrement: true,
                    allowNull: false,
                },
                title: { type: 'TEXT', allowNull: true },
            });
            queryInterface.showIndex.mockResolvedValue([]);
            queryInterface.sequelize.getDialect.mockReturnValue('sqlite');

            await safeChangeColumn(queryInterface, 'tasks', 'title', {
                type: { toSql: () => 'VARCHAR(255)' },
                allowNull: false,
            });

            const calls = queryInterface.sequelize.query.mock.calls.map(
                (c) => c[0]
            );
            expect(
                calls.some((q) => q.includes('CREATE TABLE tasks_new'))
            ).toBe(true);
            const createCall = calls.find((q) =>
                q.includes('CREATE TABLE tasks_new')
            );
            expect(createCall).toContain('VARCHAR(255)');
            expect(createCall).toContain('NOT NULL');
        });

        it('should re-throw errors', async () => {
            queryInterface.showAllTables.mockResolvedValue(['tasks']);
            queryInterface.describeTable.mockResolvedValue({ id: {}, col: {} });
            queryInterface.sequelize.getDialect.mockReturnValue('postgres');
            queryInterface.changeColumn.mockRejectedValue(
                new Error('Change failed')
            );

            await expect(
                safeChangeColumn(queryInterface, 'tasks', 'col', {
                    type: 'TEXT',
                })
            ).rejects.toThrow('Change failed');
        });
    });
});
