import { NetaClawDataSourceEntity } from '../src/modules/netaclaw/entity/data_source.js';
import {
  MysqlIntrospectionService,
  mapMysqlSchemaRows,
} from '../src/modules/netaclaw/service/mysql_schema.js';

function makeSource(overrides: Partial<NetaClawDataSourceEntity> = {}) {
  return Object.assign(new NetaClawDataSourceEntity(), {
    id: 42,
    name: 'orders',
    type: 'mysql',
    database: 'shop',
    extra: {
      allowedTables: ['customers'],
      blockedTables: [],
      maskedColumns: { 'customers.phone': 'partial' },
    },
    ...overrides,
  });
}

describe('mysql schema mapper', () => {
  it('maps camelCase columns, primary keys, indexes, foreign keys, and masked columns', () => {
    const schema = mapMysqlSchemaRows({
      maskedColumns: { 'customers.phone': 'partial' },
      columns: [
        {
          tableName: 'customers',
          tableComment: 'Customer records',
          columnName: 'id',
          ordinalPosition: 1,
          columnType: 'bigint unsigned',
          dataType: 'bigint',
          isNullable: 'NO',
          default: null,
          columnComment: 'Primary id',
          columnKey: 'PRI',
        },
        {
          tableName: 'customers',
          tableComment: 'Customer records',
          columnName: 'phone',
          ordinalPosition: 2,
          columnType: 'varchar(32)',
          dataType: 'varchar',
          isNullable: 'YES',
          default: null,
          columnComment: 'Phone number',
          columnKey: '',
        },
        {
          tableName: 'orders',
          tableComment: 'Orders',
          columnName: 'customer_id',
          ordinalPosition: 1,
          columnType: 'bigint unsigned',
          dataType: 'bigint',
          isNullable: 'NO',
          default: null,
          columnComment: '',
          columnKey: 'MUL',
        },
      ],
      indexes: [
        {
          tableName: 'customers',
          indexName: 'PRIMARY',
          nonUnique: 0,
          seqInIndex: 1,
          columnName: 'id',
        },
        {
          tableName: 'orders',
          indexName: 'idx_orders_customer',
          nonUnique: 1,
          seqInIndex: 1,
          columnName: 'customer_id',
        },
      ],
      foreignKeys: [
        {
          tableName: 'orders',
          constraintName: 'fk_orders_customer',
          columnName: 'customer_id',
          referencedTableName: 'customers',
          referencedColumnName: 'id',
          ordinalPosition: 1,
        },
      ],
    });

    expect(schema).toEqual({
      tables: [
        {
          name: 'customers',
          comment: 'Customer records',
          columns: [
            {
              name: 'id',
              type: 'bigint unsigned',
              dataType: 'bigint',
              nullable: false,
              default: null,
              comment: 'Primary id',
              ordinalPosition: 1,
            },
            {
              name: 'phone',
              type: 'varchar(32)',
              dataType: 'varchar',
              nullable: true,
              default: null,
              comment: 'Phone number',
              ordinalPosition: 2,
              masked: true,
              maskMode: 'partial',
            },
          ],
          primaryKey: ['id'],
          indexes: [
            {
              name: 'PRIMARY',
              unique: true,
              columns: ['id'],
            },
          ],
          foreignKeys: [],
        },
        {
          name: 'orders',
          comment: 'Orders',
          columns: [
            {
              name: 'customer_id',
              type: 'bigint unsigned',
              dataType: 'bigint',
              nullable: false,
              default: null,
              comment: '',
              ordinalPosition: 1,
            },
          ],
          primaryKey: [],
          indexes: [
            {
              name: 'idx_orders_customer',
              unique: false,
              columns: ['customer_id'],
            },
          ],
          foreignKeys: [
            {
              name: 'fk_orders_customer',
              columns: ['customer_id'],
              referencedTable: 'customers',
              referencedColumns: ['id'],
            },
          ],
        },
      ],
    });
  });

  it('keeps uppercase information_schema alias compatibility', () => {
    const schema = mapMysqlSchemaRows({
      columns: [
        {
          TABLE_NAME: 'customers',
          TABLE_COMMENT: 'Customer records',
          COLUMN_NAME: 'id',
          ORDINAL_POSITION: 1,
          COLUMN_TYPE: 'bigint unsigned',
          DATA_TYPE: 'bigint',
          IS_NULLABLE: 'NO',
          COLUMN_DEFAULT: null,
          COLUMN_COMMENT: 'Primary id',
          COLUMN_KEY: 'PRI',
        },
      ],
      indexes: [
        {
          TABLE_NAME: 'customers',
          INDEX_NAME: 'PRIMARY',
          NON_UNIQUE: 0,
          SEQ_IN_INDEX: 1,
          COLUMN_NAME: 'id',
        },
      ],
    });

    expect(schema.tables[0]).toMatchObject({
      name: 'customers',
      comment: 'Customer records',
      columns: [
        expect.objectContaining({
          name: 'id',
          type: 'bigint unsigned',
          dataType: 'bigint',
          nullable: false,
        }),
      ],
      primaryKey: ['id'],
      indexes: [{ name: 'PRIMARY', unique: true, columns: ['id'] }],
    });
  });
});

describe('MysqlIntrospectionService sampleTable', () => {
  it('defaults to unmasked visible columns when sampling a table with masked columns', async () => {
    const query = jest.fn().mockResolvedValue([[{ id: 1, name: 'Ada' }]]);
    const service = new MysqlIntrospectionService();
    service.mysqlPoolManager = {
      getPool: jest.fn().mockResolvedValue({ query }),
    } as any;
    service.listSchema = jest.fn().mockResolvedValue({
      tables: [
        {
          name: 'customers',
          comment: '',
          columns: [
            { name: 'id', type: 'int', dataType: 'int', nullable: false, default: null, comment: '', ordinalPosition: 1 },
            { name: 'name', type: 'varchar(64)', dataType: 'varchar', nullable: true, default: null, comment: '', ordinalPosition: 2 },
            { name: 'phone', type: 'varchar(32)', dataType: 'varchar', nullable: true, default: null, comment: '', ordinalPosition: 3, masked: true, maskMode: 'partial' },
          ],
          primaryKey: ['id'],
          indexes: [],
          foreignKeys: [],
        },
      ],
    });

    const result = await service.sampleTable(makeSource(), { table: 'customers' });

    expect(query).toHaveBeenCalledWith(
      'SELECT `id`, `name` FROM `customers` LIMIT ?',
      [5]
    );
    expect(result).toEqual({
      columns: ['id', 'name'],
      rows: [{ id: 1, name: 'Ada' }],
      rowCount: 1,
      truncated: false,
    });
  });

  it('preserves original camelCase column names when default sampling', async () => {
    const query = jest.fn().mockResolvedValue([[{ tenantId: null, isCrewMaster: 0 }]]);
    const service = new MysqlIntrospectionService();
    service.mysqlPoolManager = {
      getPool: jest.fn().mockResolvedValue({ query }),
    } as any;
    service.listSchema = jest.fn().mockResolvedValue({
      tables: [
        {
          name: 'netaclaw_agent',
          comment: '',
          columns: [
            { name: 'tenantId', type: 'bigint', dataType: 'bigint', nullable: true, default: null, comment: '', ordinalPosition: 1 },
            { name: 'isCrewMaster', type: 'tinyint', dataType: 'tinyint', nullable: false, default: 0, comment: '', ordinalPosition: 2 },
          ],
          primaryKey: [],
          indexes: [],
          foreignKeys: [],
        },
      ],
    });

    const result = await service.sampleTable(makeSource({
      extra: {
        allowedTables: ['netaclaw_agent'],
        blockedTables: [],
      },
    }), { table: 'netaclaw_agent', limit: 20 });

    expect(query).toHaveBeenCalledWith(
      'SELECT `tenantId`, `isCrewMaster` FROM `netaclaw_agent` LIMIT ?',
      [20]
    );
    expect(result.columns).toEqual(['tenantId', 'isCrewMaster']);
  });

  it('resolves explicit camelCase columns case-insensitively for sampling', async () => {
    const query = jest.fn().mockResolvedValue([[{ tenantId: null }]]);
    const service = new MysqlIntrospectionService();
    service.mysqlPoolManager = {
      getPool: jest.fn().mockResolvedValue({ query }),
    } as any;
    service.listSchema = jest.fn().mockResolvedValue({
      tables: [
        {
          name: 'netaclaw_agent',
          comment: '',
          columns: [
            { name: 'tenantId', type: 'bigint', dataType: 'bigint', nullable: true, default: null, comment: '', ordinalPosition: 1 },
          ],
          primaryKey: [],
          indexes: [],
          foreignKeys: [],
        },
      ],
    });

    const result = await service.sampleTable(makeSource({
      extra: {
        allowedTables: ['netaclaw_agent'],
        blockedTables: [],
      },
    }), { table: 'netaclaw_agent', columns: ['tenantid'] });

    expect(query).toHaveBeenCalledWith(
      'SELECT `tenantId` FROM `netaclaw_agent` LIMIT ?',
      [5]
    );
    expect(result.columns).toEqual(['tenantId']);
  });

  it('rejects explicit masked columns when sampling', async () => {
    const query = jest.fn();
    const service = new MysqlIntrospectionService();
    service.mysqlPoolManager = {
      getPool: jest.fn().mockResolvedValue({ query }),
    } as any;
    service.listSchema = jest.fn().mockResolvedValue({
      tables: [
        {
          name: 'customers',
          comment: '',
          columns: [
            { name: 'phone', type: 'varchar(32)', dataType: 'varchar', nullable: true, default: null, comment: '', ordinalPosition: 1, masked: true, maskMode: 'partial' },
          ],
          primaryKey: [],
          indexes: [],
          foreignKeys: [],
        },
      ],
    });

    await expect(service.sampleTable(makeSource(), {
      table: 'customers',
      columns: ['phone'],
    })).rejects.toThrow('masked_column_denied');

    expect(query).not.toHaveBeenCalled();
  });

  it('rejects default sampling when no unmasked columns remain', async () => {
    const service = new MysqlIntrospectionService();
    service.mysqlPoolManager = {
      getPool: jest.fn(),
    } as any;
    service.listSchema = jest.fn().mockResolvedValue({
      tables: [
        {
          name: 'customers',
          comment: '',
          columns: [
            { name: 'phone', type: 'varchar(32)', dataType: 'varchar', nullable: true, default: null, comment: '', ordinalPosition: 1, masked: true, maskMode: 'partial' },
          ],
          primaryKey: [],
          indexes: [],
          foreignKeys: [],
        },
      ],
    });

    await expect(service.sampleTable(makeSource(), {
      table: 'customers',
    })).rejects.toThrow('masked_column_denied');
  });

  it('rejects sampling unallowed name-only tables under all-names-only schema visibility', async () => {
    const service = new MysqlIntrospectionService();
    service.mysqlPoolManager = {
      getPool: jest.fn(),
    } as any;

    await expect(service.sampleTable(makeSource({
      extra: {
        allowedTables: ['customers'],
        blockedTables: [],
        schemaVisibility: 'all-names-only',
      },
    }), {
      table: 'orders',
    })).rejects.toThrow('table_not_allowed');
  });
});

describe('MysqlIntrospectionService listSchema visibility', () => {
  it('returns names only for requested unblocked tables outside allowedTables when schemaVisibility is all-names-only', async () => {
    const query = jest.fn()
      .mockResolvedValueOnce([
        [
          { TABLE_NAME: 'customers', TABLE_COMMENT: 'Allowed customers' },
          { TABLE_NAME: 'orders', TABLE_COMMENT: 'Visible name only' },
        ],
      ])
      .mockResolvedValueOnce([
        [
          {
            TABLE_NAME: 'customers',
            TABLE_COMMENT: 'Allowed customers',
            COLUMN_NAME: 'id',
            ORDINAL_POSITION: 1,
            COLUMN_TYPE: 'int',
            DATA_TYPE: 'int',
            IS_NULLABLE: 'NO',
            COLUMN_DEFAULT: null,
            COLUMN_COMMENT: '',
            COLUMN_KEY: 'PRI',
          },
        ],
      ])
      .mockResolvedValueOnce([[{ TABLE_NAME: 'customers', INDEX_NAME: 'PRIMARY', NON_UNIQUE: 0, SEQ_IN_INDEX: 1, COLUMN_NAME: 'id' }]])
      .mockResolvedValueOnce([[]]);
    const service = new MysqlIntrospectionService();
    service.mysqlPoolManager = {
      getPool: jest.fn().mockResolvedValue({ query }),
    } as any;

    const schema = await service.listSchema(makeSource({
      extra: {
        allowedTables: ['customers'],
        blockedTables: ['payments'],
        schemaVisibility: 'all-names-only',
      },
    }), { tables: ['customers', 'orders', 'payments'] });

    expect(schema.tables).toEqual([
      {
        name: 'customers',
        comment: 'Allowed customers',
        columns: [
          expect.objectContaining({ name: 'id' }),
        ],
        primaryKey: ['id'],
        indexes: [{ name: 'PRIMARY', unique: true, columns: ['id'] }],
        foreignKeys: [],
      },
      {
        name: 'orders',
        comment: 'Visible name only',
        columns: [],
        primaryKey: [],
        indexes: [],
        foreignKeys: [],
      },
    ]);
    expect(query).toHaveBeenNthCalledWith(
      1,
      expect.stringContaining('information_schema.TABLES'),
      ['shop', 'customers', 'orders']
    );
    expect(query).toHaveBeenNthCalledWith(
      2,
      expect.stringContaining('information_schema.COLUMNS'),
      ['shop', 'customers']
    );
  });

  it('does not request TABLE_COMMENT from information_schema.COLUMNS', async () => {
    const query = jest.fn()
      .mockResolvedValueOnce([
        [
          { TABLE_NAME: 'customers', TABLE_COMMENT: 'Allowed customers' },
        ],
      ])
      .mockResolvedValueOnce([
        [
          {
            TABLE_NAME: 'customers',
            COLUMN_NAME: 'id',
            ORDINAL_POSITION: 1,
            COLUMN_TYPE: 'int',
            DATA_TYPE: 'int',
            IS_NULLABLE: 'NO',
            COLUMN_DEFAULT: null,
            COLUMN_COMMENT: '',
            COLUMN_KEY: 'PRI',
          },
        ],
      ])
      .mockResolvedValueOnce([[{ TABLE_NAME: 'customers', INDEX_NAME: 'PRIMARY', NON_UNIQUE: 0, SEQ_IN_INDEX: 1, COLUMN_NAME: 'id' }]])
      .mockResolvedValueOnce([[]]);
    const service = new MysqlIntrospectionService();
    service.mysqlPoolManager = {
      getPool: jest.fn().mockResolvedValue({ query }),
    } as any;

    const schema = await service.listSchema(makeSource());

    expect(query).toHaveBeenNthCalledWith(
      1,
      expect.stringContaining('information_schema.TABLES'),
      ['shop', 'customers']
    );
    expect(query).toHaveBeenNthCalledWith(
      2,
      expect.not.stringContaining('TABLE_COMMENT'),
      ['shop', 'customers']
    );
    expect(schema.tables[0].comment).toBe('Allowed customers');
  });
});
