import { schemaDiff } from 'src/sql-tools/schema-diff';
import {
  ActionType,
  ColumnType,
  ConstraintType,
  DatabaseColumn,
  DatabaseConstraint,
  DatabaseIndex,
  DatabaseSchema,
  DatabaseTable,
} from 'src/sql-tools/types';
import { describe, expect, it } from 'vitest';

const fromColumn = (column: Partial<Omit<DatabaseColumn, 'tableName'>>): DatabaseSchema => {
  const tableName = 'table1';

  return {
    databaseName: 'postgres',
    schemaName: 'public',
    functions: [],
    enums: [],
    extensions: [],
    parameters: [],
    overrides: [],
    tables: [
      {
        name: tableName,
        columns: [
          {
            name: 'column1',
            primary: false,
            synchronize: true,
            isArray: false,
            type: 'character varying',
            nullable: false,
            ...column,
            tableName,
          },
        ],
        indexes: [],
        triggers: [],
        constraints: [],
        synchronize: true,
      },
    ],
    warnings: [],
  };
};

const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => {
  const tableName = constraint?.tableName || 'table1';

  return {
    databaseName: 'postgres',
    schemaName: 'public',
    functions: [],
    enums: [],
    extensions: [],
    parameters: [],
    overrides: [],
    tables: [
      {
        name: tableName,
        columns: [
          {
            name: 'column1',
            primary: false,
            synchronize: true,
            isArray: false,
            type: 'character varying',
            nullable: false,
            tableName,
          },
        ],
        indexes: [],
        triggers: [],
        constraints: constraint ? [constraint] : [],
        synchronize: true,
      },
    ],
    warnings: [],
  };
};

const fromIndex = (index?: DatabaseIndex): DatabaseSchema => {
  const tableName = index?.tableName || 'table1';

  return {
    databaseName: 'postgres',
    schemaName: 'public',
    functions: [],
    enums: [],
    extensions: [],
    parameters: [],
    overrides: [],
    tables: [
      {
        name: tableName,
        columns: [
          {
            name: 'column1',
            primary: false,
            synchronize: true,
            isArray: false,
            type: 'character varying',
            nullable: false,
            tableName,
          },
        ],
        indexes: index ? [index] : [],
        constraints: [],
        triggers: [],
        synchronize: true,
      },
    ],
    warnings: [],
  };
};

const newSchema = (schema: {
  name?: string;
  tables: Array<{
    name: string;
    columns?: Array<{
      name: string;
      type?: ColumnType;
      nullable?: boolean;
      isArray?: boolean;
    }>;
    indexes?: DatabaseIndex[];
    constraints?: DatabaseConstraint[];
  }>;
}): DatabaseSchema => {
  const tables: DatabaseTable[] = [];

  for (const table of schema.tables || []) {
    const tableName = table.name;
    const columns: DatabaseColumn[] = [];

    for (const column of table.columns || []) {
      const columnName = column.name;

      columns.push({
        tableName,
        name: columnName,
        primary: false,
        type: column.type || 'character varying',
        isArray: column.isArray ?? false,
        nullable: column.nullable ?? false,
        synchronize: true,
      });
    }

    tables.push({
      name: tableName,
      columns,
      indexes: table.indexes ?? [],
      constraints: table.constraints ?? [],
      triggers: [],
      synchronize: true,
    });
  }

  return {
    databaseName: 'immich',
    schemaName: schema?.name || 'public',
    functions: [],
    enums: [],
    extensions: [],
    parameters: [],
    overrides: [],
    tables,
    warnings: [],
  };
};

describe(schemaDiff.name, () => {
  it('should work', () => {
    const diff = schemaDiff(newSchema({ tables: [] }), newSchema({ tables: [] }));
    expect(diff.items).toEqual([]);
  });

  describe('table', () => {
    describe('TableCreate', () => {
      it('should find a missing table', () => {
        const column: DatabaseColumn = {
          type: 'character varying',
          tableName: 'table1',
          primary: false,
          name: 'column1',
          isArray: false,
          nullable: false,
          synchronize: true,
        };
        const diff = schemaDiff(
          newSchema({ tables: [{ name: 'table1', columns: [column] }] }),
          newSchema({ tables: [] }),
        );

        expect(diff.items).toHaveLength(1);
        expect(diff.items[0]).toEqual({
          type: 'TableCreate',
          table: {
            name: 'table1',
            columns: [column],
            constraints: [],
            indexes: [],
            triggers: [],
            synchronize: true,
          },
          reason: 'missing in target',
        });
      });
    });

    describe('TableDrop', () => {
      it('should find an extra table', () => {
        const diff = schemaDiff(
          newSchema({ tables: [] }),
          newSchema({
            tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
          }),
          { tables: { ignoreExtra: false } },
        );

        expect(diff.items).toHaveLength(1);
        expect(diff.items[0]).toEqual({
          type: 'TableDrop',
          tableName: 'table1',
          reason: 'missing in source',
        });
      });
    });

    it('should skip identical tables', () => {
      const diff = schemaDiff(
        newSchema({
          tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
        }),
        newSchema({
          tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
        }),
      );

      expect(diff.items).toEqual([]);
    });
  });

  describe('column', () => {
    describe('ColumnAdd', () => {
      it('should find a new column', () => {
        const diff = schemaDiff(
          newSchema({
            tables: [
              {
                name: 'table1',
                columns: [{ name: 'column1' }, { name: 'column2' }],
              },
            ],
          }),
          newSchema({
            tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
          }),
        );

        expect(diff.items).toEqual([
          {
            type: 'ColumnAdd',
            column: {
              tableName: 'table1',
              isArray: false,
              primary: false,
              name: 'column2',
              nullable: false,
              type: 'character varying',
              synchronize: true,
            },
            reason: 'missing in target',
          },
        ]);
      });
    });

    describe('ColumnDrop', () => {
      it('should find an extra column', () => {
        const diff = schemaDiff(
          newSchema({
            tables: [{ name: 'table1', columns: [{ name: 'column1' }] }],
          }),
          newSchema({
            tables: [
              {
                name: 'table1',
                columns: [{ name: 'column1' }, { name: 'column2' }],
              },
            ],
          }),
        );

        expect(diff.items).toEqual([
          {
            type: 'ColumnDrop',
            tableName: 'table1',
            columnName: 'column2',
            reason: 'missing in source',
          },
        ]);
      });
    });

    describe('nullable', () => {
      it('should make a column nullable', () => {
        const diff = schemaDiff(
          fromColumn({ name: 'column1', nullable: true }),
          fromColumn({ name: 'column1', nullable: false }),
        );

        expect(diff.items).toEqual([
          {
            type: 'ColumnAlter',
            tableName: 'table1',
            columnName: 'column1',
            changes: {
              nullable: true,
            },
            reason: 'nullable is different (true vs false)',
          },
        ]);
      });

      it('should make a column non-nullable', () => {
        const diff = schemaDiff(
          fromColumn({ name: 'column1', nullable: false }),
          fromColumn({ name: 'column1', nullable: true }),
        );

        expect(diff.items).toEqual([
          {
            type: 'ColumnAlter',
            tableName: 'table1',
            columnName: 'column1',
            changes: {
              nullable: false,
            },
            reason: 'nullable is different (false vs true)',
          },
        ]);
      });
    });

    describe('default', () => {
      it('should set a default value to a function', () => {
        const diff = schemaDiff(
          fromColumn({ name: 'column1', default: 'uuid_generate_v4()' }),
          fromColumn({ name: 'column1' }),
        );

        expect(diff.items).toEqual([
          {
            type: 'ColumnAlter',
            tableName: 'table1',
            columnName: 'column1',
            changes: {
              default: 'uuid_generate_v4()',
            },
            reason: 'default is different (uuid_generate_v4() vs undefined)',
          },
        ]);
      });

      it('should ignore explicit casts for strings', () => {
        const diff = schemaDiff(
          fromColumn({ name: 'column1', type: 'character varying', default: `''` }),
          fromColumn({ name: 'column1', type: 'character varying', default: `''::character varying` }),
        );

        expect(diff.items).toEqual([]);
      });

      it('should ignore explicit casts for numbers', () => {
        const diff = schemaDiff(
          fromColumn({ name: 'column1', type: 'bigint', default: `0` }),
          fromColumn({ name: 'column1', type: 'bigint', default: `'0'::bigint` }),
        );

        expect(diff.items).toEqual([]);
      });

      it('should ignore explicit casts for enums', () => {
        const diff = schemaDiff(
          fromColumn({ name: 'column1', type: 'enum', enumName: 'enum1', default: `test` }),
          fromColumn({ name: 'column1', type: 'enum', enumName: 'enum1', default: `'test'::enum1` }),
        );

        expect(diff.items).toEqual([]);
      });

      it('should support arrays, ignoring types', () => {
        const diff = schemaDiff(
          fromColumn({ name: 'column1', type: 'character varying', isArray: true, default: "'{}'" }),
          fromColumn({
            name: 'column1',
            type: 'character varying',
            isArray: true,
            default: "'{}'::character varying[]",
          }),
        );

        expect(diff.items).toEqual([]);
      });
    });
  });

  describe('constraint', () => {
    describe('ConstraintAdd', () => {
      it('should detect a new constraint', () => {
        const diff = schemaDiff(
          fromConstraint({
            name: 'PK_test',
            type: ConstraintType.PRIMARY_KEY,
            tableName: 'table1',
            columnNames: ['id'],
            synchronize: true,
          }),
          fromConstraint(),
        );

        expect(diff.items).toEqual([
          {
            type: 'ConstraintAdd',
            constraint: {
              type: ConstraintType.PRIMARY_KEY,
              name: 'PK_test',
              columnNames: ['id'],
              tableName: 'table1',
              synchronize: true,
            },
            reason: 'missing in target',
          },
        ]);
      });
    });

    describe('ConstraintDrop', () => {
      it('should detect an extra constraint', () => {
        const diff = schemaDiff(
          fromConstraint(),
          fromConstraint({
            name: 'PK_test',
            type: ConstraintType.PRIMARY_KEY,
            tableName: 'table1',
            columnNames: ['id'],
            synchronize: true,
          }),
        );

        expect(diff.items).toEqual([
          {
            type: 'ConstraintDrop',
            tableName: 'table1',
            constraintName: 'PK_test',
            reason: 'missing in source',
          },
        ]);
      });
    });

    describe('primary key', () => {
      it('should skip identical primary key constraints', () => {
        const constraint: DatabaseConstraint = {
          type: ConstraintType.PRIMARY_KEY,
          name: 'PK_test',
          tableName: 'table1',
          columnNames: ['id'],
          synchronize: true,
        };

        const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint }));

        expect(diff.items).toEqual([]);
      });
    });

    describe('foreign key', () => {
      it('should skip identical foreign key constraints', () => {
        const constraint: DatabaseConstraint = {
          type: ConstraintType.FOREIGN_KEY,
          name: 'FK_test',
          tableName: 'table1',
          columnNames: ['parentId'],
          referenceTableName: 'table2',
          referenceColumnNames: ['id'],
          synchronize: true,
        };

        const diff = schemaDiff(fromConstraint(constraint), fromConstraint(constraint));

        expect(diff.items).toEqual([]);
      });

      it('should drop and recreate when the column changes', () => {
        const constraint: DatabaseConstraint = {
          type: ConstraintType.FOREIGN_KEY,
          name: 'FK_test',
          tableName: 'table1',
          columnNames: ['parentId'],
          referenceTableName: 'table2',
          referenceColumnNames: ['id'],
          synchronize: true,
        };

        const diff = schemaDiff(
          fromConstraint(constraint),
          fromConstraint({ ...constraint, columnNames: ['parentId2'] }),
        );

        expect(diff.items).toEqual([
          {
            constraintName: 'FK_test',
            reason: 'columns are different (parentId vs parentId2)',
            tableName: 'table1',
            type: 'ConstraintDrop',
          },
          {
            constraint: {
              columnNames: ['parentId'],
              name: 'FK_test',
              referenceColumnNames: ['id'],
              referenceTableName: 'table2',
              synchronize: true,
              tableName: 'table1',
              type: 'foreign-key',
            },
            reason: 'columns are different (parentId vs parentId2)',
            type: 'ConstraintAdd',
          },
        ]);
      });

      it('should drop and recreate when the ON DELETE action changes', () => {
        const constraint: DatabaseConstraint = {
          type: ConstraintType.FOREIGN_KEY,
          name: 'FK_test',
          tableName: 'table1',
          columnNames: ['parentId'],
          referenceTableName: 'table2',
          referenceColumnNames: ['id'],
          onDelete: ActionType.CASCADE,
          synchronize: true,
        };

        const diff = schemaDiff(fromConstraint(constraint), fromConstraint({ ...constraint, onDelete: undefined }));

        expect(diff.items).toEqual([
          {
            constraintName: 'FK_test',
            reason: 'ON DELETE action is different (CASCADE vs NO ACTION)',
            tableName: 'table1',
            type: 'ConstraintDrop',
          },
          {
            constraint: {
              columnNames: ['parentId'],
              name: 'FK_test',
              referenceColumnNames: ['id'],
              referenceTableName: 'table2',
              onDelete: ActionType.CASCADE,
              synchronize: true,
              tableName: 'table1',
              type: 'foreign-key',
            },
            reason: 'ON DELETE action is different (CASCADE vs NO ACTION)',
            type: 'ConstraintAdd',
          },
        ]);
      });
    });

    describe('unique', () => {
      it('should skip identical unique constraints', () => {
        const constraint: DatabaseConstraint = {
          type: ConstraintType.UNIQUE,
          name: 'UQ_test',
          tableName: 'table1',
          columnNames: ['id'],
          synchronize: true,
        };

        const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint }));

        expect(diff.items).toEqual([]);
      });
    });

    describe('check', () => {
      it('should skip identical check constraints', () => {
        const constraint: DatabaseConstraint = {
          type: ConstraintType.CHECK,
          name: 'CHK_test',
          tableName: 'table1',
          expression: 'column1 > 0',
          synchronize: true,
        };

        const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint }));

        expect(diff.items).toEqual([]);
      });
    });
  });

  describe('index', () => {
    describe('IndexCreate', () => {
      it('should detect a new index', () => {
        const diff = schemaDiff(
          fromIndex({
            name: 'IDX_test',
            tableName: 'table1',
            columnNames: ['id'],
            unique: false,
            synchronize: true,
          }),
          fromIndex(),
        );

        expect(diff.items).toEqual([
          {
            type: 'IndexCreate',
            index: {
              name: 'IDX_test',
              columnNames: ['id'],
              tableName: 'table1',
              unique: false,
              synchronize: true,
            },
            reason: 'missing in target',
          },
        ]);
      });
    });

    describe('IndexDrop', () => {
      it('should detect an extra index', () => {
        const diff = schemaDiff(
          fromIndex(),
          fromIndex({
            name: 'IDX_test',
            unique: true,
            tableName: 'table1',
            columnNames: ['id'],
            synchronize: true,
          }),
        );

        expect(diff.items).toEqual([
          {
            type: 'IndexDrop',
            indexName: 'IDX_test',
            reason: 'missing in source',
          },
        ]);
      });
    });

    it('should recreate the index if unique changes', () => {
      const index: DatabaseIndex = {
        name: 'IDX_test',
        tableName: 'table1',
        columnNames: ['id'],
        unique: true,
        synchronize: true,
      };
      const diff = schemaDiff(fromIndex(index), fromIndex({ ...index, unique: false }));

      expect(diff.items).toEqual([
        {
          type: 'IndexDrop',
          indexName: 'IDX_test',
          reason: 'uniqueness is different (true vs false)',
        },
        {
          type: 'IndexCreate',
          index,
          reason: 'uniqueness is different (true vs false)',
        },
      ]);
    });
  });
});
