import { describe, test, expect, vi, beforeEach } from 'vitest';
import { DatabaseAdvanceService } from '../../src/services/database/database-advance.service';
import { AppError } from '../../src/utils/errors';
import { ERROR_CODES } from '@insforge/shared-schemas';

describe('DatabaseAdvanceService - sanitizeQuery', () => {
  const service = DatabaseAdvanceService.getInstance();

  test('blocks database-level operations', () => {
    const queries = [
      'DROP DATABASE customer_project',
      'CREATE DATABASE customer_project',
      'ALTER DATABASE customer_project SET timezone TO UTC',
    ];

    for (const query of queries) {
      expect(() => service.sanitizeQuery(query)).toThrow(AppError);
    }
  });

  test('blocks role and session authorization management', () => {
    const queries = [
      'SET ROLE postgres',
      'SET LOCAL ROLE postgres',
      'RESET ROLE',
      'SET SESSION AUTHORIZATION postgres',
      'RESET SESSION AUTHORIZATION',
      'RESET ALL',
      'SET search_path TO public',
      "SELECT set_config('search_path', 'public', false)",
      'SET statement_timeout = 0',
      'RESET statement_timeout',
      'CREATE ROLE app_owner',
      'ALTER ROLE project_admin SET search_path TO public',
      'DROP ROLE app_owner',
      'GRANT postgres TO project_admin',
    ];

    for (const query of queries) {
      expect(() => service.sanitizeQuery(query)).toThrow(AppError);
    }
  });

  test('blocks transaction control in raw SQL', () => {
    const queries = ['BEGIN', 'COMMIT', 'ROLLBACK', 'SAVEPOINT before_change'];

    for (const query of queries) {
      expect(() => service.sanitizeQuery(query)).toThrow(AppError);
    }
  });

  test('throws AppError with 403 FORBIDDEN for execution context violations', () => {
    try {
      service.sanitizeQuery('RESET ROLE');
      expect.fail('Should have thrown an error');
    } catch (error) {
      expect(error).toBeInstanceOf(AppError);
      if (error instanceof AppError) {
        expect(error.statusCode).toBe(403);
        expect(error.code).toBe(ERROR_CODES.FORBIDDEN);
        expect(error.message).toContain('execution role');
      }
    }
  });

  test('allows managed schema statements to be decided by project_admin database grants', () => {
    const queries = [
      "INSERT INTO auth.users (email, password_hash) VALUES ('demo@example.com', 'hash')",
      'CREATE TRIGGER user_profile_trigger AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.create_user_profile()',
      'SELECT * FROM pg_catalog.pg_class LIMIT 1',
      "INSERT INTO storage.objects (bucket_id, key, name) VALUES ('avatars', 'u1/a.png', 'a.png')",
      'ALTER TABLE realtime.messages ENABLE ROW LEVEL SECURITY',
      "UPDATE payments.customers SET email = 'new@example.com' WHERE id = 'cus_123'",
      "INSERT INTO system.custom_migrations (version, name, statements) VALUES ('1', 'manual', ARRAY['SELECT 1'])",
    ];

    for (const query of queries) {
      expect(() => service.sanitizeQuery(query)).not.toThrow();
    }
  });

  test('allows public schema DDL and grants', () => {
    const queries = [
      'CREATE TABLE public.products (id uuid PRIMARY KEY)',
      'ALTER TABLE public.products ENABLE ROW LEVEL SECURITY',
      'CREATE POLICY products_select ON public.products FOR SELECT TO authenticated USING (true)',
      'GRANT SELECT ON public.products TO authenticated',
      'DROP POLICY products_select ON public.products',
    ];

    for (const query of queries) {
      expect(() => service.sanitizeQuery(query)).not.toThrow();
    }
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// bulkInsert — optional-column data-loss fix (issue #8)
// ─────────────────────────────────────────────────────────────────────────────

const { mockClientQuery, mockRelease, mockConnect } = vi.hoisted(() => ({
  mockClientQuery: vi.fn(),
  mockRelease: vi.fn(),
  mockConnect: vi.fn(),
}));

vi.mock('../../src/infra/database/database.manager', () => ({
  DatabaseManager: {
    getInstance: vi.fn(() => ({
      getPool: vi.fn(() => ({
        query: vi.fn(),
        connect: mockConnect,
      })),
    })),
  },
}));

describe('DatabaseAdvanceService - bulkInsert optional-column fix', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // pool.connect() returns a pg client with query + release
    mockConnect.mockResolvedValue({
      query: mockClientQuery,
      release: mockRelease,
    });
    // Default: all queries succeed
    mockClientQuery.mockResolvedValue({ rows: [], rowCount: 2 });
  });

  /** Helper: find all INSERT SQL queries executed */
  function captureAllInsertSql(): string[] {
    return mockClientQuery.mock.calls
      .map(([sql]) => sql as string)
      .filter((sql) => typeof sql === 'string' && sql.startsWith('INSERT INTO'));
  }

  test('groups records by shape and executes shape-coherent batch queries', async () => {
    const svc = DatabaseAdvanceService.getInstance();

    await svc.bulkInsert('public', 'contacts', [
      { id: '1', name: 'Alice' },
      { id: '2', name: 'Bob', phone: '555-0001' },
      { id: '3', name: 'Carol', phone: '555-0002' },
    ]);

    const sqlQueries = captureAllInsertSql();
    expect(sqlQueries).toHaveLength(2);

    // First query (Alice) has no phone
    const query1 = sqlQueries[0];
    expect(query1).toContain('id');
    expect(query1).toContain('name');
    expect(query1).not.toContain('phone');
    expect(query1).toContain('Alice');

    // Second query (Bob and Carol) has phone
    const query2 = sqlQueries[1];
    expect(query2).toContain('id');
    expect(query2).toContain('name');
    expect(query2).toContain('phone');
    expect(query2).toContain('555-0001');
    expect(query2).toContain('555-0002');
  });

  test('explicit undefined values are converted to NULL', async () => {
    const svc = DatabaseAdvanceService.getInstance();

    await svc.bulkInsert('public', 'contacts', [{ id: '1', name: 'Alice', phone: undefined }]);

    const sqlQueries = captureAllInsertSql();
    expect(sqlQueries).toHaveLength(1);
    expect(sqlQueries[0]).toContain('phone');
    expect(sqlQueries[0]).not.toContain('undefined');
    expect(sqlQueries[0]).toContain('NULL');
  });

  test('uniform records (all same columns) still work correctly', async () => {
    const svc = DatabaseAdvanceService.getInstance();

    await svc.bulkInsert('public', 'contacts', [
      { id: '1', name: 'Alice', phone: '555-0001' },
      { id: '2', name: 'Bob', phone: '555-0002' },
    ]);

    const sqlQueries = captureAllInsertSql();
    expect(sqlQueries).toHaveLength(1);
    const sql = sqlQueries[0];
    expect(sql).toContain('id');
    expect(sql).toContain('name');
    expect(sql).toContain('phone');
    expect(sql).toContain('555-0001');
    expect(sql).toContain('555-0002');
  });

  test('throws AppError when records array is empty', async () => {
    const svc = DatabaseAdvanceService.getInstance();
    await expect(svc.bulkInsert('public', 'contacts', [])).rejects.toBeInstanceOf(AppError);
  });

  test('upsert: optional column from later records is included in ON CONFLICT SET for its group', async () => {
    const svc = DatabaseAdvanceService.getInstance();

    await svc.bulkInsert(
      'public',
      'contacts',
      [
        { id: '1', name: 'Alice' },
        { id: '2', name: 'Bob', phone: '555-0001' },
      ],
      'id'
    );

    const sqlQueries = captureAllInsertSql();
    expect(sqlQueries).toHaveLength(2);

    const query1 = sqlQueries[0]; // Alice: keys ['id', 'name']
    expect(query1).toContain('ON CONFLICT');
    expect(query1).toContain('DO UPDATE SET');
    expect(query1).toContain('name = EXCLUDED.name');
    expect(query1).not.toContain('phone');

    const query2 = sqlQueries[1]; // Bob: keys ['id', 'name', 'phone']
    expect(query2).toContain('ON CONFLICT');
    expect(query2).toContain('DO UPDATE SET');
    expect(query2).toContain('name = EXCLUDED.name');
    expect(query2).toContain('phone = EXCLUDED.phone');
  });

  test('explicit null values are preserved in the SQL', async () => {
    const svc = DatabaseAdvanceService.getInstance();

    await svc.bulkInsert('public', 'contacts', [{ id: '1', name: 'Alice', phone: null }]);

    const sqlQueries = captureAllInsertSql();
    expect(sqlQueries).toHaveLength(1);
    expect(sqlQueries[0]).toContain('phone');
    expect(sqlQueries[0]).toContain('NULL');
  });

  test('rolls back entire transaction if one shape group insert fails', async () => {
    const svc = DatabaseAdvanceService.getInstance();

    mockClientQuery.mockReset();
    mockClientQuery
      .mockResolvedValueOnce({}) // SET ROLE project_admin
      .mockResolvedValueOnce({}) // set_config claims
      .mockResolvedValueOnce({}) // BEGIN
      .mockResolvedValueOnce({ rows: [], rowCount: 1 }) // First INSERT (Alice)
      .mockRejectedValueOnce(new Error('Constraint violation on second group')) // Second INSERT (Bob)
      .mockResolvedValueOnce({}) // ROLLBACK
      .mockResolvedValueOnce({}) // RESET ROLE
      .mockResolvedValueOnce({}); // set_config empty claims

    const records = [
      { id: '1', name: 'Alice' },
      { id: '2', name: 'Bob', phone: '555-0001' },
    ];

    await expect(svc.bulkInsert('public', 'contacts', records)).rejects.toThrow(
      'Constraint violation on second group'
    );

    const calls = mockClientQuery.mock.calls.map(([sql]) => sql);
    expect(calls).toContain('BEGIN');
    expect(calls).toContain('ROLLBACK');
    expect(calls).not.toContain('COMMIT');
  });
});
