import { TABLE_EVENT_ACTIONS } from 'common/telemetry-constants'
import { describe, expect, it } from 'vitest'

import { sqlEventParser } from './sql-event-parser'

describe('SQL Event Parser', () => {
  describe('CREATE TABLE detection', () => {
    it('detects basic CREATE TABLE', () => {
      const results = sqlEventParser.getTableEvents('CREATE TABLE users (id INT PRIMARY KEY)')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'users',
      })
    })

    it('detects CREATE TABLE with schema', () => {
      const results = sqlEventParser.getTableEvents('CREATE TABLE public.users (id INT)')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: 'public',
        tableName: 'users',
      })
    })

    it('detects CREATE TABLE IF NOT EXISTS', () => {
      const results = sqlEventParser.getTableEvents('CREATE TABLE IF NOT EXISTS users (id INT)')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'users',
      })
    })

    it('handles quoted identifiers', () => {
      const results = sqlEventParser.getTableEvents('CREATE TABLE "public"."user_table" (id INT)')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: 'public',
        tableName: 'user_table',
      })
    })

    it('returns empty array for non-matching SQL', () => {
      const results = sqlEventParser.getTableEvents('SELECT * FROM users')
      expect(results).toHaveLength(0)
    })

    it('detects CREATE TEMPORARY TABLE', () => {
      const results = sqlEventParser.getTableEvents('CREATE TEMPORARY TABLE temp_users (id INT)')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'temp_users',
      })
    })

    it('detects CREATE TEMP TABLE', () => {
      const results = sqlEventParser.getTableEvents('CREATE TEMP TABLE temp_users (id INT)')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'temp_users',
      })
    })

    it('detects CREATE UNLOGGED TABLE', () => {
      const results = sqlEventParser.getTableEvents('CREATE UNLOGGED TABLE fast_table (id INT)')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'fast_table',
      })
    })

    it('detects CREATE TEMP TABLE IF NOT EXISTS', () => {
      const results = sqlEventParser.getTableEvents(
        'CREATE TEMP TABLE IF NOT EXISTS temp_users (id INT)'
      )
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'temp_users',
      })
    })
  })

  describe('INSERT detection', () => {
    it('detects basic INSERT INTO', () => {
      const results = sqlEventParser.getTableEvents("INSERT INTO users (name) VALUES ('John')")
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableDataAdded,
        schema: undefined,
        tableName: 'users',
      })
    })

    it('detects INSERT with schema', () => {
      const results = sqlEventParser.getTableEvents(
        "INSERT INTO public.users (name) VALUES ('John')"
      )
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableDataAdded,
        schema: 'public',
        tableName: 'users',
      })
    })

    it('handles quoted identifiers', () => {
      const results = sqlEventParser.getTableEvents('INSERT INTO "auth"."users" (id) VALUES (1)')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableDataAdded,
        schema: 'auth',
        tableName: 'users',
      })
    })

    it('returns empty array for non-matching SQL', () => {
      const results = sqlEventParser.getTableEvents('UPDATE users SET name = "John"')
      expect(results).toHaveLength(0)
    })
  })

  describe('COPY detection', () => {
    it('detects basic COPY FROM', () => {
      const results = sqlEventParser.getTableEvents("COPY users FROM '/tmp/users.csv'")
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableDataAdded,
        schema: undefined,
        tableName: 'users',
      })
    })

    it('detects COPY with schema', () => {
      const results = sqlEventParser.getTableEvents(
        "COPY public.users FROM '/tmp/users.csv' WITH CSV HEADER"
      )
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableDataAdded,
        schema: 'public',
        tableName: 'users',
      })
    })

    it('handles quoted identifiers', () => {
      const results = sqlEventParser.getTableEvents('COPY "auth"."users" FROM STDIN')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableDataAdded,
        schema: 'auth',
        tableName: 'users',
      })
    })

    it('returns empty array for COPY TO', () => {
      const results = sqlEventParser.getTableEvents("COPY users TO '/tmp/users.csv'")
      expect(results).toHaveLength(0)
    })

    it('returns empty array for non-matching SQL', () => {
      const results = sqlEventParser.getTableEvents('SELECT * FROM users')
      expect(results).toHaveLength(0)
    })
  })

  describe('SELECT INTO detection', () => {
    it('detects SELECT INTO', () => {
      const results = sqlEventParser.getTableEvents('SELECT * INTO new_users FROM users')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'new_users',
      })
    })

    it('detects SELECT INTO with schema', () => {
      const results = sqlEventParser.getTableEvents(
        'SELECT id, name INTO public.new_users FROM users'
      )
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: 'public',
        tableName: 'new_users',
      })
    })

    it('detects CREATE TABLE AS SELECT', () => {
      const results = sqlEventParser.getTableEvents('CREATE TABLE new_users AS SELECT * FROM users')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'new_users',
      })
    })

    it('detects CREATE TABLE IF NOT EXISTS AS SELECT', () => {
      const results = sqlEventParser.getTableEvents(
        'CREATE TABLE IF NOT EXISTS new_users AS SELECT * FROM users WHERE active = true'
      )
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'new_users',
      })
    })

    it('handles quoted identifiers', () => {
      const results = sqlEventParser.getTableEvents(
        'SELECT * INTO "backup"."users_2024" FROM users'
      )
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: 'backup',
        tableName: 'users_2024',
      })
    })

    it('returns empty array for regular SELECT', () => {
      const results = sqlEventParser.getTableEvents('SELECT * FROM users')
      expect(results).toHaveLength(0)
    })
  })

  describe('RLS detection', () => {
    it('detects ALTER TABLE ENABLE ROW LEVEL SECURITY', () => {
      const results = sqlEventParser.getTableEvents('ALTER TABLE users ENABLE ROW LEVEL SECURITY')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableRLSEnabled,
        schema: undefined,
        tableName: 'users',
      })
    })

    it('detects short form ENABLE RLS', () => {
      const results = sqlEventParser.getTableEvents('ALTER TABLE users ENABLE RLS')
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableRLSEnabled,
        schema: undefined,
        tableName: 'users',
      })
    })

    it('detects with schema', () => {
      const results = sqlEventParser.getTableEvents(
        'ALTER TABLE public.users ENABLE ROW LEVEL SECURITY'
      )
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableRLSEnabled,
        schema: 'public',
        tableName: 'users',
      })
    })

    it('handles other ALTER TABLE statements in between', () => {
      const results = sqlEventParser.getTableEvents(
        'ALTER TABLE users ADD COLUMN test INT, ENABLE ROW LEVEL SECURITY'
      )
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableRLSEnabled,
        schema: undefined,
        tableName: 'users',
      })
    })

    it('returns empty array for disabling RLS', () => {
      const results = sqlEventParser.getTableEvents('ALTER TABLE users DISABLE ROW LEVEL SECURITY')
      expect(results).toHaveLength(0)
    })

    it('detects ALTER TABLE IF EXISTS ENABLE ROW LEVEL SECURITY', () => {
      const results = sqlEventParser.getTableEvents(
        'ALTER TABLE IF EXISTS public."Conversations" ENABLE ROW LEVEL SECURITY'
      )
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableRLSEnabled,
        schema: 'public',
        tableName: 'Conversations',
      })
    })

    it('detects ALTER TABLE ONLY ENABLE ROW LEVEL SECURITY', () => {
      const results = sqlEventParser.getTableEvents(
        'ALTER TABLE ONLY public.users ENABLE ROW LEVEL SECURITY'
      )
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableRLSEnabled,
        schema: 'public',
        tableName: 'users',
      })
    })

    it('detects ALTER TABLE IF EXISTS ONLY ENABLE ROW LEVEL SECURITY', () => {
      const results = sqlEventParser.getTableEvents(
        'ALTER TABLE IF EXISTS ONLY public.users ENABLE ROW LEVEL SECURITY'
      )
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableRLSEnabled,
        schema: 'public',
        tableName: 'users',
      })
    })
  })

  describe('ReDoS protection', () => {
    it('handles extremely long identifier names efficiently', () => {
      const longIdentifier = 'a'.repeat(10000)
      const sql = `CREATE TABLE ${longIdentifier} (id INT)`

      const startTime = Date.now()
      const results = sqlEventParser.getTableEvents(sql)
      const duration = Date.now() - startTime

      expect(duration).toBeLessThan(100)
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: longIdentifier,
      })
    })

    it('handles nested dots in schema names without catastrophic backtracking', () => {
      const maliciousInput = 'a.'.repeat(1000) + 'table'
      const sql = `CREATE TABLE ${maliciousInput} (id INT)`

      const startTime = Date.now()
      const results = sqlEventParser.getTableEvents(sql)
      const duration = Date.now() - startTime

      expect(duration).toBeLessThan(100)
      expect(results.length).toBeGreaterThan(0)
    })

    it('handles pathological SELECT INTO patterns', () => {
      const maliciousSQL = 'SELECT ' + 'a '.repeat(1000) + 'INTO table FROM users'

      const startTime = Date.now()
      const results = sqlEventParser.getTableEvents(maliciousSQL)
      const duration = Date.now() - startTime

      expect(duration).toBeLessThan(100)
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'table',
      })
    })

    it('handles ALTER TABLE with many operations between', () => {
      const manyOperations = 'ADD COLUMN test INT, '.repeat(100)
      const sql = `ALTER TABLE users ${manyOperations} ENABLE ROW LEVEL SECURITY`

      const startTime = Date.now()
      const results = sqlEventParser.getTableEvents(sql)
      const duration = Date.now() - startTime

      expect(duration).toBeLessThan(100)
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableRLSEnabled,
        schema: undefined,
        tableName: 'users',
      })
    })

    it('handles mixed quotes and backticks efficiently', () => {
      const mixedQuotes = '`"`.'.repeat(100) + 'tablename'
      const sql = `CREATE TABLE ${mixedQuotes} (id INT)`

      const startTime = Date.now()
      sqlEventParser.getTableEvents(sql)
      const duration = Date.now() - startTime

      expect(duration).toBeLessThan(100)
    })
  })

  describe('Edge cases and special characters', () => {
    it('handles Unicode identifiers', () => {
      const sql = 'CREATE TABLE 用户表 (id INT)'
      const results = sqlEventParser.getTableEvents(sql)
      expect(results).toHaveLength(0)
    })

    it('handles identifiers with numbers', () => {
      const sql = 'CREATE TABLE table123 (id INT)'
      const results = sqlEventParser.getTableEvents(sql)
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'table123',
      })
    })

    it('handles identifiers with underscores', () => {
      const sql = 'CREATE TABLE user_accounts (id INT)'
      const results = sqlEventParser.getTableEvents(sql)
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'user_accounts',
      })
    })

    it('handles escaped quotes in identifiers', () => {
      const sql = 'CREATE TABLE "user""table" (id INT)'
      const results = sqlEventParser.getTableEvents(sql)
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'usertable',
      })
    })

    it('does not scan inside dollar-quoted string literals', () => {
      // $$...$$ is a string literal in Postgres — its contents must not be
      // parsed as DDL, otherwise a user inserting SQL-shaped text into a log
      // table would trigger false-positive table-created events.
      const sql = `
        CREATE TABLE users (id INT);
        INSERT INTO logs VALUES ($$CREATE TABLE fake$$);
        INSERT INTO users VALUES (1);
      `
      const results = sqlEventParser.getTableEvents(sql)
      expect(results).toHaveLength(3)
      expect(results[0]).toMatchObject({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        tableName: 'users',
      })
      expect(results[1]).toMatchObject({
        type: TABLE_EVENT_ACTIONS.TableDataAdded,
        tableName: 'logs',
      })
      expect(results[2]).toMatchObject({
        type: TABLE_EVENT_ACTIONS.TableDataAdded,
        tableName: 'users',
      })
    })

    it('does not treat SELECT..INTO inside a plpgsql body as table creation', () => {
      // Regression for the RLS warning modal false-positive: variable
      // assignment inside a function body must not be reported as a new table.
      const sql = `
        create or replace function schema_checks()
        returns jsonb
        language plpgsql
        as $$
        declare
          ret jsonb;
        begin
          select jsonb_build_object('value', 'ok') into ret;
          return ret;
        end;
        $$;
      `
      const results = sqlEventParser.getTableEvents(sql)
      expect(results).toEqual([])
    })

    it('does not leak nested dollar-quoted dynamic SQL through statement splitting', () => {
      // Regression: an inner $sql$...$sql$ tag used inside an outer $fn$...$fn$
      // body was pairing with the outer opening tag (the splitStatements regex
      // doesn't enforce matching tags), which caused the inner semicolon to
      // split the statement and exposed `create table fake` to the detectors.
      // The fix is that stripDollarQuoteBodies runs before splitStatements and
      // uses a backreference to require matching tags.
      const sql = `
        create function f()
        returns void
        language plpgsql
        as $fn$
        begin
          execute $sql$create table fake(id int);$sql$;
        end;
        $fn$;
      `
      const results = sqlEventParser.getTableEvents(sql)
      expect(results).toEqual([])
    })

    it('still detects a real top-level CREATE TABLE next to a function with nested dollar tags', () => {
      const sql = `
        create table public.real_table(id int);
        create function f()
        returns void
        language plpgsql
        as $fn$
        begin
          execute $sql$create table fake(id int);$sql$;
        end;
        $fn$;
      `
      const results = sqlEventParser.getTableEvents(sql)
      expect(results).toEqual([
        {
          type: TABLE_EVENT_ACTIONS.TableCreated,
          schema: 'public',
          tableName: 'real_table',
        },
      ])
    })

    it('handles SQL injection attempts safely', () => {
      const sql = "CREATE TABLE users'; DROP TABLE users; -- (id INT)"
      const results = sqlEventParser.getTableEvents(sql)
      expect(results).toHaveLength(1)
      expect(results[0]).toEqual({
        type: TABLE_EVENT_ACTIONS.TableCreated,
        schema: undefined,
        tableName: 'users',
      })
    })
  })

  describe('getTableEvents', () => {
    it('filters only table-related events', () => {
      const sql = `
        CREATE TABLE users (id INT);
        CREATE FUNCTION test() RETURNS INT AS $$ BEGIN RETURN 1; END; $$ LANGUAGE plpgsql;
        INSERT INTO users (id) VALUES (1);
        ALTER TABLE users ENABLE RLS;
        CREATE VIEW user_view AS SELECT * FROM users;
      `
      const results = sqlEventParser.getTableEvents(sql)
      expect(results).toHaveLength(3)
      expect(results.map((r) => r.type)).toEqual([
        TABLE_EVENT_ACTIONS.TableCreated,
        TABLE_EVENT_ACTIONS.TableDataAdded,
        TABLE_EVENT_ACTIONS.TableRLSEnabled,
      ])
    })

    it('returns empty array for non-table SQL', () => {
      const sql = `
        CREATE FUNCTION test() RETURNS INT AS $$ BEGIN RETURN 1; END; $$ LANGUAGE plpgsql;
        CREATE VIEW user_view AS SELECT * FROM users;
        SELECT * FROM users;
      `
      const results = sqlEventParser.getTableEvents(sql)
      expect(results).toHaveLength(0)
    })
  })
})
