import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { sql } from 'drizzle-orm';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createPgliteDb } from './client-pglite.js';
import { runPgliteMigrations } from './migrate.js';
import type { DbHandle } from './client.js';

interface PgliteExec {
  exec(query: string): Promise<unknown>;
}

describe('runPgliteMigrations', () => {
  let dataDir: string;
  let handle: DbHandle;

  beforeEach(() => {
    dataDir = mkdtempSync(join(tmpdir(), 'mosaic-db-migrate-test-'));
    handle = createPgliteDb(dataDir);
  });

  afterEach(async () => {
    await handle.close();
    rmSync(dataDir, { recursive: true, force: true });
  });

  it('creates the BetterAuth tables required by the gateway', async () => {
    await runPgliteMigrations(handle);

    const result = (await handle.db.execute(sql`
      SELECT table_name FROM information_schema.tables
      WHERE table_schema = 'public'
      ORDER BY table_name
    `)) as unknown as { rows: Array<{ table_name: string }> };

    const tables = result.rows.map((r) => r.table_name);

    // Auth tables — required for sign-in / bootstrap to function.
    expect(tables).toContain('users');
    expect(tables).toContain('sessions');
    expect(tables).toContain('accounts');
    expect(tables).toContain('verifications');

    // Schema sanity check — admin token table consumed by mosaic gateway config.
    expect(tables).toContain('admin_tokens');
  });

  it('is idempotent — running twice does not error', async () => {
    await runPgliteMigrations(handle);
    await expect(runPgliteMigrations(handle)).resolves.toBeUndefined();
  });

  it('gives accounts an issuer column (#1395) — better-auth >=1.7 requires it', async () => {
    await runPgliteMigrations(handle);

    const result = (await handle.db.execute(sql`
      SELECT column_name, is_nullable, data_type
      FROM information_schema.columns
      WHERE table_name = 'accounts' AND column_name = 'issuer'
    `)) as unknown as {
      rows: Array<{ column_name: string; is_nullable: string; data_type: string }>;
    };

    // Nullable by design: the 1.5.x line this repo's lockfile resolves to does
    // not write the field; 1.7+ populates it. One schema serves both.
    expect(result.rows).toHaveLength(1);
    expect(result.rows[0]?.is_nullable).toBe('YES');
    expect(result.rows[0]?.data_type).toBe('text');
  });

  it('backfills ONLY credential rows with the synthetic issuer (#1395 upgrade path)', async () => {
    // Simulate an upgraded install: migrate through 0016 only, seed pre-issuer
    // rows (one credential, one oauth), then apply 0017 and discriminate.
    const client = (handle.db as unknown as { $client: PgliteExec }).$client;

    // Migrate to 0016 by replaying every ledger file except 0017 — the ledger
    // table gates re-application, so a plain replay of 0000..0016 is enough.
    const fs = await import('node:fs');
    const path = await import('node:path');
    const dir = path.join(import.meta.dirname, '..', 'drizzle');
    const files = fs
      .readdirSync(dir)
      .filter((f) => /^\d{4}_.*\.sql$/.test(f) && f < '0017')
      .sort();
    for (const f of files) {
      const raw = fs.readFileSync(path.join(dir, f), 'utf-8');
      for (const stmt of raw.split('--> statement-breakpoint')) {
        const trimmed = stmt.trim();
        if (trimmed) await client.exec(trimmed);
      }
    }

    await client.exec(`
      INSERT INTO users (id, name, email, email_verified, created_at, updated_at)
      VALUES ('u1', 'Legacy User', 'legacy@example.test', true, now(), now());
      INSERT INTO accounts (id, account_id, provider_id, user_id, created_at, updated_at)
      VALUES
        ('a1', 'legacy@example.test', 'credential', 'u1', now(), now()),
        ('a2', 'oauth-provider-1', 'google', 'u1', now(), now());
    `);

    // Apply 0017 (column + backfill).
    const sql0017 = fs.readFileSync(path.join(dir, '0017_accounts_issuer.sql'), 'utf-8');
    for (const stmt of sql0017.split('--> statement-breakpoint')) {
      const trimmed = stmt.trim();
      if (trimmed) await client.exec(trimmed);
    }

    const rows = (await handle.db.execute(sql`
      SELECT provider_id, issuer FROM accounts ORDER BY id
    `)) as unknown as { rows: Array<{ provider_id: string; issuer: string | null }> };

    const byProvider = new Map(rows.rows.map((r) => [r.provider_id, r.issuer]));
    // Credential rows get better-auth's synthetic local issuer — the value
    // sign-in filters on (better-auth dist createLocalAccountIssuer).
    expect(byProvider.get('credential')).toBe('local:credential');
    // OAuth rows are LEFT NULL: better-auth owns their issuer semantics going
    // forward (each provider's real issuer on its next flow).
    expect(byProvider.get('google')).toBeNull();
  });

  it('surfaces statement-level error context on failure and leaves no ledger row', async () => {
    // Pre-create a `users` table that conflicts with migration 0000's CREATE TABLE,
    // forcing it to fail without IF NOT EXISTS.
    const client = (handle.db as unknown as { $client: PgliteExec }).$client;
    await client.exec('CREATE TABLE users (sentinel text)');

    await expect(runPgliteMigrations(handle)).rejects.toThrow(
      /migration hash=[a-f0-9]+ statement #\d+ failed/,
    );

    // Ledger should be empty — partial application must not pretend to be complete.
    const ledger = (await handle.db.execute(
      sql`SELECT count(*)::int AS count FROM drizzle.__drizzle_migrations`,
    )) as unknown as { rows: Array<{ count: number }> };
    expect(ledger.rows[0]?.count).toBe(0);
  });
});
