import { describe, expect, it } from 'vitest'

import {
  getFacetCountQuery,
  getLogsChartQuery,
  getLogsCountQuery,
  getUnifiedLogsQuery,
} from './UnifiedLogs.queries'
import { getUnifiedLogsQuery as getUnifiedLogsQueryBQ } from './UnifiedLogs.queries.bq'

const baseSearch = {
  date: [new Date('2026-05-08T09:00:00Z'), new Date('2026-05-08T10:00:00Z')],
} as any

describe('UnifiedLogs.queries (OTEL flat)', () => {
  describe('getUnifiedLogsQuery', () => {
    it('defaults to postgres + postgrest log types when none specified', () => {
      const sql = getUnifiedLogsQuery(baseSearch)
      expect(sql).toContain(`source = 'postgres_logs'`)
      // postgrest = edge_logs filtered by /rest/ path
      expect(sql).toContain(
        `source = 'edge_logs' AND log_attributes['request.path'] LIKE '%/rest/%'`
      )
    })

    it('routes the `edge` log type to edge_logs without /rest/ or /storage/ paths', () => {
      const sql = getUnifiedLogsQuery({ ...baseSearch, log_type: ['edge'] } as any)
      expect(sql).toContain(`NOT LIKE '%/rest/%'`)
      expect(sql).toContain(`NOT LIKE '%/storage/%'`)
      const where = sql.split(/\bWHERE\b/)[1] ?? ''
      expect(where).not.toContain(`source = 'postgres_logs'`)
    })

    it('routes the `storage` log type to edge_logs filtered by /storage/', () => {
      const sql = getUnifiedLogsQuery({ ...baseSearch, log_type: ['storage'] } as any)
      expect(sql).toContain(
        `source = 'edge_logs' AND log_attributes['request.path'] LIKE '%/storage/%'`
      )
    })

    it('escapes single quotes in filter values to prevent SQL injection', () => {
      const sql = getUnifiedLogsQuery({
        ...baseSearch,
        method: [`G'ET`],
        pathname: `/customers'; DROP TABLE logs --`,
      } as any)
      // Single quotes are doubled (SQL-standard escaping) by pg-meta's
      // literal(); a raw single quote from user input never closes its
      // string literal early.
      expect(sql).toContain(`'G''ET'`)
      expect(sql).toContain(`%/customers''; DROP TABLE logs --%`)
    })

    it('translates method/status/pathname filters to log_attributes predicates', () => {
      const sql = getUnifiedLogsQuery({
        ...baseSearch,
        method: ['GET'],
        status: ['401'],
        pathname: '/customers',
      } as any)
      expect(sql).toContain(`log_attributes['request.method'] IN ('GET')`)
      // Status filter wraps the CASE that picks HTTP code or Postgres SQLSTATE
      // so e.g. '00000' matches postgres success rows.
      expect(sql).toContain(`log_attributes['parsed.sql_state_code']`)
      expect(sql).toMatch(/END\) IN \('401'\)/)
      expect(sql).toContain(`log_attributes['request.path'] LIKE '%/customers%'`)
    })

    it('does not emit subqueries or CTEs (rejected by the OTEL endpoint)', () => {
      const sql = getUnifiedLogsQuery(baseSearch)
      expect(sql).not.toMatch(/WITH\s+\w+\s+AS\s*\(/i)
      expect(sql).not.toMatch(/FROM\s*\(\s*SELECT/i)
      // Single SELECT * FROM logs (not "SELECT *" wildcard usage either).
      expect(sql).not.toMatch(/SELECT\s+\*/)
    })
  })

  describe('getLogsCountQuery', () => {
    it('emits one UNION ALL branch per log_type bucket and per level', () => {
      const sql = getLogsCountQuery(baseSearch)
      // Per-log-type counts
      for (const lt of ['edge', 'postgrest', 'storage', 'postgres', 'edge function', 'auth']) {
        expect(sql).toContain(`'${lt}'`)
      }
      // Per-level counts
      for (const lvl of ['success', 'warning', 'error']) {
        expect(sql).toContain(`'${lvl}'`)
      }
      // Bundled via UNION ALL — multiple occurrences expected
      expect(sql.match(/UNION ALL/g)?.length ?? 0).toBeGreaterThan(5)
    })

    it('honours an active log_type filter in the total count branch', () => {
      const sql = getLogsCountQuery({ ...baseSearch, log_type: ['edge'] } as any)
      // The first branch is the total — its WHERE must include the edge
      // log_type predicate, otherwise the total badge would over-count
      // when a log_type filter is active.
      const totalBranch = sql.split(/\bUNION ALL\b/)[0]
      expect(totalBranch).toContain(`'total'`)
      expect(totalBranch).toContain(`source = 'edge_logs'`)
      expect(totalBranch).not.toContain(`source = 'postgres_logs'`)
    })
  })

  describe('getLogsChartQuery', () => {
    it('uses minute bucketing for short ranges', () => {
      const sql = getLogsChartQuery(baseSearch)
      expect(sql).toContain('toStartOfMinute(timestamp)')
    })

    it('uses hour bucketing for ranges spanning more than 12 hours', () => {
      const sql = getLogsChartQuery({
        ...baseSearch,
        date: [new Date('2026-05-08T00:00:00Z'), new Date('2026-05-08T18:00:00Z')],
      } as any)
      expect(sql).toContain('toStartOfHour(timestamp)')
    })

    it('uses day bucketing for ranges spanning more than 2 days', () => {
      const sql = getLogsChartQuery({
        ...baseSearch,
        date: [new Date('2026-05-01T00:00:00Z'), new Date('2026-05-08T00:00:00Z')],
      } as any)
      expect(sql).toContain('toStartOfDay(timestamp)')
    })
  })

  describe('getFacetCountQuery', () => {
    it('groups by the requested facet and excludes that facet from the WHERE filters', () => {
      const sql = getFacetCountQuery({
        search: { ...baseSearch, method: ['GET'], status: ['200'] } as any,
        facet: 'method',
      })
      // Filtered facet is excluded from WHERE; other filters still applied.
      // For facet='method' the SELECT projection doesn't include STATUS_EXPR,
      // so SQLSTATE/IN ('200') must come from the WHERE clause.
      expect(sql).not.toContain(`log_attributes['request.method'] IN ('GET')`)
      expect(sql).toContain(`log_attributes['parsed.sql_state_code']`)
      expect(sql).toMatch(/END\) IN \('200'\)/)
      expect(sql).toContain('GROUP BY value')
      expect(sql).toContain('LIMIT 20')
    })
  })

  describe('analyticsLiteral escaping', () => {
    it('emits ClickHouse / BigQuery escape syntax (doubled `\\\\`, no `E` prefix)', () => {
      // pg-meta's literal() would emit `E'a\\b'` for `a\b` — the `E` prefix is
      // Postgres-only and rejected by both analytics engines. analyticsLiteral
      // doubles the backslash inside plain `'…'` delimiters instead.
      const sql = getUnifiedLogsQuery({ ...baseSearch, method: 'a\\b' } as any)
      expect(sql).toContain(`log_attributes['request.method'] = 'a\\\\b'`)
      expect(sql).not.toContain(`E'a`)
    })

    it("escapes single quotes by doubling them ('' rather than \\')", () => {
      const sql = getUnifiedLogsQuery({ ...baseSearch, method: "GET' OR '1'='1" } as any)
      expect(sql).toContain(`log_attributes['request.method'] = 'GET'' OR ''1''=''1'`)
    })
  })
})

describe('UnifiedLogs.queries.bq', () => {
  it('backtick-quotes column identifiers (BigQuery syntax)', () => {
    const sql = getUnifiedLogsQueryBQ({ ...baseSearch, method: 'GET' } as any)
    expect(sql).toContain("`method` = 'GET'")
  })

  it('rejects keys with non-identifier characters', () => {
    // A key like "foo; DROP TABLE" fails the bqIdent regex (the `;` is not
    // in `[A-Za-z_][A-Za-z0-9_]*`), so the predicate is dropped entirely.
    const sql = getUnifiedLogsQueryBQ({ ...baseSearch, 'foo; DROP TABLE x': 'y' } as any)
    expect(sql).not.toContain('DROP TABLE')
    expect(sql).not.toContain('foo;')
  })

  it('rejects keys containing spaces', () => {
    const sql = getUnifiedLogsQueryBQ({
      ...baseSearch,
      'level OR id IS NOT NULL': 'anything',
    } as any)
    expect(sql).not.toContain('IS NOT NULL')
    expect(sql).not.toContain('level OR')
  })

  it('escapes injection attempts in filter values via analyticsLiteral', () => {
    const sql = getUnifiedLogsQueryBQ({ ...baseSearch, method: "GET' OR '1'='1" } as any)
    // The value is single-quote-escaped, so the synthetic OR can't break out
    // of the string literal.
    expect(sql).toContain("`method` = 'GET'' OR ''1''=''1'")
    expect(sql).not.toMatch(/`method` = 'GET' OR '1'='1'/)
  })
})
