import {
  extractMysqlTables,
  validateMysqlReadOnlySql,
} from '../src/modules/netaclaw/service/mysql_query.js';

describe('mysql sql guard', () => {
  const baseOptions = {
    allowedTables: ['customers', 'orders', 'order_items', 'products'],
    maxJoinTables: 4,
    maxRows: 100,
  };

  it('accepts SELECT with explicit JOIN condition and extracts normalized tables', () => {
    const sql =
      'SELECT c.id, o.total FROM `Customers` c JOIN `Orders` o ON o.customer_id = c.id LIMIT 20';

    const result = validateMysqlReadOnlySql(sql, baseOptions);

    expect(result).toMatchObject({
      ok: true,
      tables: ['customers', 'orders'],
      limitedSql: sql,
      appendedLimit: false,
    });
    expect(extractMysqlTables(sql)).toEqual(['customers', 'orders']);
  });

  it('accepts a single trailing semicolon and normalizes it out', () => {
    const result = validateMysqlReadOnlySql('SELECT * FROM customers LIMIT 10;', baseOptions);

    expect(result).toMatchObject({
      ok: true,
      tables: ['customers'],
      limitedSql: 'SELECT * FROM customers LIMIT 10',
      appendedLimit: false,
    });
  });

  it.each([
    ['SHOW TABLES', 'metadata_sql_denied'],
    ['DESCRIBE customers', 'metadata_sql_denied'],
    ['EXPLAIN SELECT * FROM customers', 'metadata_sql_denied'],
    ['UPDATE customers SET name = "x"', 'dml_sql_denied'],
    ['DELETE FROM customers', 'dml_sql_denied'],
    ['DROP TABLE customers', 'ddl_sql_denied'],
    ['SELECT * FROM customers; SELECT * FROM orders', 'multiple_statements_denied'],
    ['SELECT * FROM customers -- trailing comment', 'comments_denied'],
    ['WITH c AS (SELECT * FROM customers) SELECT * FROM c', 'cte_denied'],
    ['SELECT * FROM customers UNION SELECT * FROM orders', 'union_denied'],
    ['SELECT @rownum := @rownum + 1 FROM customers', 'user_variable_denied'],
    ['SELECT SLEEP(1) FROM customers', 'dangerous_function_denied'],
    ["SELECT GET_LOCK('neta_lock', 30) FROM customers LIMIT 1", 'dangerous_function_denied'],
    ["SELECT RELEASE_LOCK('neta_lock') FROM customers LIMIT 1", 'dangerous_function_denied'],
    ["SELECT IS_FREE_LOCK('neta_lock') FROM customers LIMIT 1", 'dangerous_function_denied'],
    ["SELECT IS_USED_LOCK('neta_lock') FROM customers LIMIT 1", 'dangerous_function_denied'],
    ['SELECT * FROM customers INTO OUTFILE "/tmp/customers.csv"', 'file_output_denied'],
  ])('rejects unsafe SQL: %s', (sql, reason) => {
    expect(validateMysqlReadOnlySql(sql, baseOptions)).toMatchObject({
      ok: false,
      reason,
    });
  });

  it('rejects unallowed tables', () => {
    expect(
      validateMysqlReadOnlySql('SELECT * FROM payments LIMIT 10', baseOptions)
    ).toMatchObject({
      ok: false,
      reason: 'table_not_allowed',
      tables: ['payments'],
    });
  });

  it('rejects schema-qualified table references', () => {
    expect(
      validateMysqlReadOnlySql('SELECT * FROM otherdb.customers LIMIT 10', baseOptions)
    ).toMatchObject({
      ok: false,
      reason: 'schema_qualified_table_denied',
    });
  });

  it('rejects blocked tables', () => {
    expect(
      validateMysqlReadOnlySql('SELECT * FROM orders LIMIT 10', {
        ...baseOptions,
        blockedTables: ['orders'],
      })
    ).toMatchObject({
      ok: false,
      reason: 'table_blocked',
      tables: ['orders'],
    });
  });

  it('rejects JOIN without ON or USING', () => {
    expect(
      validateMysqlReadOnlySql(
        'SELECT * FROM customers JOIN orders WHERE customers.id = orders.customer_id LIMIT 10',
        baseOptions
      )
    ).toMatchObject({
      ok: false,
      reason: 'join_condition_required',
    });
  });

  it('rejects too many joined tables', () => {
    expect(
      validateMysqlReadOnlySql(
        'SELECT * FROM customers JOIN orders ON orders.customer_id = customers.id JOIN order_items ON order_items.order_id = orders.id JOIN products ON products.id = order_items.product_id LIMIT 10',
        { ...baseOptions, maxJoinTables: 2 }
      )
    ).toMatchObject({
      ok: false,
      reason: 'too_many_join_tables',
      tables: ['customers', 'orders', 'order_items', 'products'],
    });
  });

  it('defaults maxJoinTables to 4', () => {
    const options = {
      allowedTables: ['customers', 'orders', 'order_items', 'products', 'regions'],
      maxRows: 100,
    };

    expect(
      validateMysqlReadOnlySql(
        'SELECT * FROM customers JOIN orders ON orders.customer_id = customers.id JOIN order_items ON order_items.order_id = orders.id JOIN products ON products.id = order_items.product_id LIMIT 10',
        options
      )
    ).toMatchObject({
      ok: true,
      tables: ['customers', 'orders', 'order_items', 'products'],
    });

    expect(
      validateMysqlReadOnlySql(
        'SELECT * FROM customers JOIN orders ON orders.customer_id = customers.id JOIN order_items ON order_items.order_id = orders.id JOIN products ON products.id = order_items.product_id JOIN regions ON regions.id = customers.region_id LIMIT 10',
        options
      )
    ).toMatchObject({
      ok: false,
      reason: 'too_many_join_tables',
      tables: ['customers', 'orders', 'order_items', 'products', 'regions'],
    });
  });

  it('rejects later JOIN missing its own ON or USING', () => {
    expect(
      validateMysqlReadOnlySql(
        'SELECT * FROM customers JOIN orders ON orders.customer_id = customers.id JOIN order_items WHERE order_items.order_id = orders.id LIMIT 10',
        baseOptions
      )
    ).toMatchObject({
      ok: false,
      reason: 'join_condition_required',
    });
  });

  it('rejects explicit LIMIT above maxRows', () => {
    expect(
      validateMysqlReadOnlySql('SELECT * FROM customers LIMIT 101', baseOptions)
    ).toMatchObject({
      ok: false,
      reason: 'limit_exceeds_max_rows',
    });
  });

  it('defaults maxRows to 200', () => {
    const options = {
      allowedTables: ['customers'],
      maxJoinTables: 4,
    };

    expect(
      validateMysqlReadOnlySql('SELECT * FROM customers LIMIT 200', options)
    ).toMatchObject({
      ok: true,
      tables: ['customers'],
    });

    expect(
      validateMysqlReadOnlySql('SELECT * FROM customers LIMIT 201', options)
    ).toMatchObject({
      ok: false,
      reason: 'limit_exceeds_max_rows',
      tables: ['customers'],
    });
  });

  it('rejects masked column references', () => {
    expect(
      validateMysqlReadOnlySql('SELECT phone FROM customers LIMIT 10', {
        ...baseOptions,
        maskedColumns: { 'customers.phone': 'partial' },
      })
    ).toMatchObject({
      ok: false,
      reason: 'masked_column_denied',
      tables: ['customers'],
    });
  });

  it.each([
    'SELECT orders.phone FROM customers JOIN orders ON orders.customer_id = customers.id LIMIT 10',
    "SELECT id FROM customers WHERE note = 'phone' LIMIT 10",
  ])('does not reject safe non-customers masked-column text: %s', sql => {
    expect(
      validateMysqlReadOnlySql(sql, {
        ...baseOptions,
        maskedColumns: { 'customers.phone': 'partial' },
      })
    ).toMatchObject({
      ok: true,
    });
  });

  it.each([
    'SELECT customers.phone FROM customers LIMIT 10',
    'SELECT c.phone FROM customers c LIMIT 10',
    'SELECT phone FROM customers LIMIT 10',
  ])('rejects actual masked column references: %s', sql => {
    expect(
      validateMysqlReadOnlySql(sql, {
        ...baseOptions,
        maskedColumns: { 'customers.phone': 'partial' },
      })
    ).toMatchObject({
      ok: false,
      reason: 'masked_column_denied',
      tables: ['customers'],
    });
  });

  it.each([
    'SELECT * FROM customers LIMIT 10',
    'SELECT customers.* FROM customers LIMIT 10',
    'SELECT c.* FROM customers c LIMIT 10',
    'SELECT c2.* FROM customers c1 JOIN customers c2 ON c2.referrer_id = c1.id LIMIT 10',
    'SELECT `c-1`.* FROM customers AS `c-1` LIMIT 1',
  ])('rejects wildcard projection when referenced table has masked columns: %s', sql => {
    expect(
      validateMysqlReadOnlySql(sql, {
        ...baseOptions,
        maskedColumns: { 'customers.phone': 'partial' },
      })
    ).toMatchObject({
      ok: false,
      reason: 'masked_column_denied',
      tables: ['customers'],
    });
  });

  it('rejects derived table projections when derived query could expose masked columns', () => {
    expect(
      validateMysqlReadOnlySql('SELECT x.* FROM (SELECT * FROM customers) x LIMIT 1', {
        ...baseOptions,
        maskedColumns: { 'customers.phone': 'partial' },
      })
    ).toMatchObject({
      ok: false,
      reason: 'derived_table_denied',
      tables: ['customers'],
    });
  });

  it.each([
    'SELECT id FROM customers WHERE id = 1 FOR UPDATE',
    'SELECT id FROM customers WHERE id = 1 FOR SHARE',
    'SELECT id FROM customers WHERE id = 1 LOCK IN SHARE MODE',
  ])('rejects locking read SQL: %s', sql => {
    expect(validateMysqlReadOnlySql(sql, baseOptions)).toMatchObject({
      ok: false,
      reason: 'locking_read_denied',
      tables: ['customers'],
    });
  });

  it.each([
    'SELECT * FROM customers LIMIT 10 OFFSET 1000',
    'SELECT * FROM customers LIMIT 1000, 10',
  ])('rejects offset limits: %s', sql => {
    expect(validateMysqlReadOnlySql(sql, baseOptions)).toMatchObject({
      ok: false,
      reason: 'offset_denied',
      tables: ['customers'],
    });
  });

  it('wraps select without LIMIT in a bounded query', () => {
    expect(
      validateMysqlReadOnlySql('SELECT * FROM customers', baseOptions)
    ).toMatchObject({
      ok: true,
      limitedSql:
        'SELECT * FROM (SELECT * FROM customers) AS neta_limited_query LIMIT ?',
      appendedLimit: true,
    });
  });

  it.each([
    "SELECT id FROM customers WHERE note = 'limit 1'",
    'SELECT id FROM customers WHERE id IN (SELECT customer_id FROM orders LIMIT 1)',
  ])('ignores non-top-level LIMIT and adds an outer bounded query: %s', sql => {
    expect(validateMysqlReadOnlySql(sql, baseOptions)).toMatchObject({
      ok: true,
      limitedSql: `SELECT * FROM (${sql}) AS neta_limited_query LIMIT ?`,
      appendedLimit: true,
    });
  });

  it('rejects no-table selects when allowedTables is configured', () => {
    expect(validateMysqlReadOnlySql('SELECT DATABASE()', baseOptions)).toMatchObject({
      ok: false,
      reason: 'table_not_allowed',
      tables: [],
    });
  });
});
