import { PgTestClient } from 'insforge-test';
import { getConnections } from './utils';

let db: PgTestClient;
let teardown: () => Promise<void>;

const USER_A = '550e8400-e29b-41d4-a716-446655440001';
const USER_B = '550e8400-e29b-41d4-a716-446655440002';
const USER_C = '550e8400-e29b-41d4-a716-446655440003';

const asUser = (userId: string) => ({
  role: 'authenticated',
  'request.jwt.claims': JSON.stringify({ sub: userId }),
});

beforeAll(async () => {
  ({ db, teardown } = await getConnections());

  await db.query(`
    CREATE TABLE todos (
      id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      user_id UUID NOT NULL DEFAULT auth.uid(),
      title TEXT NOT NULL,
      done BOOLEAN DEFAULT false
    );
    ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
    CREATE POLICY "owner_select" ON todos FOR SELECT
      TO authenticated USING (auth.uid() = user_id);
    CREATE POLICY "owner_insert" ON todos FOR INSERT
      TO authenticated WITH CHECK (auth.uid() = user_id);
    CREATE POLICY "owner_update" ON todos FOR UPDATE
      TO authenticated USING (auth.uid() = user_id);
    CREATE POLICY "owner_delete" ON todos FOR DELETE
      TO authenticated USING (auth.uid() = user_id);
    GRANT ALL ON todos TO authenticated;
  `);
});

afterAll(() => teardown());
beforeEach(() => db.beforeEach());
afterEach(() => db.afterEach());

describe('owner crud', () => {
  it('should allow user to create their own todo', async () => {
    db.setContext(asUser(USER_A));

    const todo = await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING *`, ['Buy milk']);

    expect(todo.title).toBe('Buy milk');
    expect(todo.user_id).toBe(USER_A);
  });

  it('should allow user to read only their own todos', async () => {
    db.setContext(asUser(USER_A));
    await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['Task A']);

    db.setContext(asUser(USER_B));
    await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['Task B']);

    db.setContext(asUser(USER_A));
    const rows = await db.many(`SELECT * FROM todos`);
    expect(rows).toHaveLength(1);
    expect(rows[0].title).toBe('Task A');
  });

  it('should allow user to update their own todo', async () => {
    db.setContext(asUser(USER_A));
    const todo = await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['Draft']);

    const updated = await db.one(`UPDATE todos SET title = $1 WHERE id = $2 RETURNING *`, [
      'Final',
      todo.id,
    ]);
    expect(updated.title).toBe('Final');
  });

  it('should allow user to delete their own todo', async () => {
    db.setContext(asUser(USER_A));
    const todo = await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['Remove me']);

    const result = await db.query(`DELETE FROM todos WHERE id = $1`, [todo.id]);
    expect(result.rowCount).toBe(1);
  });
});

describe('cross-user isolation', () => {
  it('should prevent user from seeing another users todos', async () => {
    db.setContext(asUser(USER_A));
    await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['Secret']);

    db.setContext(asUser(USER_B));
    const { rows } = await db.query(`SELECT * FROM todos`);
    expect(rows).toHaveLength(0);
  });

  it('should prevent user from updating another users todo', async () => {
    db.setContext(asUser(USER_A));
    const todo = await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['Private']);

    db.setContext(asUser(USER_B));
    const result = await db.query(`UPDATE todos SET title = 'hacked' WHERE id = $1`, [todo.id]);
    expect(result.rowCount).toBe(0);
  });

  it('should prevent user from deleting another users todo', async () => {
    db.setContext(asUser(USER_A));
    const todo = await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['Keep me']);

    db.setContext(asUser(USER_B));
    const result = await db.query(`DELETE FROM todos WHERE id = $1`, [todo.id]);
    expect(result.rowCount).toBe(0);
  });

  it('should isolate three users from each other', async () => {
    db.setContext(asUser(USER_A));
    await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['A task']);

    db.setContext(asUser(USER_B));
    await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['B task']);

    db.setContext(asUser(USER_C));
    await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['C task']);

    // Each user sees only their own
    db.setContext(asUser(USER_A));
    const aRows = await db.many(`SELECT * FROM todos`);
    expect(aRows).toHaveLength(1);
    expect(aRows[0].title).toBe('A task');

    db.setContext(asUser(USER_B));
    const bRows = await db.many(`SELECT * FROM todos`);
    expect(bRows).toHaveLength(1);
    expect(bRows[0].title).toBe('B task');

    db.setContext(asUser(USER_C));
    const cRows = await db.many(`SELECT * FROM todos`);
    expect(cRows).toHaveLength(1);
    expect(cRows[0].title).toBe('C task');
  });
});

describe('role access control', () => {
  it('should block anon from reading any data', async () => {
    db.setContext(asUser(USER_A));
    await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['Exists']);

    db.setContext({ role: 'anon' });
    const { rows } = await db.query(`SELECT * FROM todos`);
    expect(rows).toHaveLength(0);
  });

  it('should allow project_admin to bypass RLS via BYPASSRLS role attribute', async () => {
    db.setContext(asUser(USER_A));
    await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['Admin can see me']);

    db.setContext(asUser(USER_B));
    await db.one(`INSERT INTO todos (title) VALUES ($1) RETURNING id`, ['Admin sees this too']);

    // project_admin has BYPASSRLS — no per-table policy needed
    db.setContext({ role: 'project_admin' });
    const rows = await db.many(`SELECT * FROM todos`);
    expect(rows.length).toBeGreaterThanOrEqual(2);
  });

  it('should confirm no project_admin_policy exists on the table', async () => {
    const { rows } = await db.query(
      `SELECT policyname FROM pg_policies WHERE tablename = 'todos' AND policyname = 'project_admin_policy'`
    );
    expect(rows).toHaveLength(0);
  });
});
