import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import postgres from 'postgres';
import { PostgresSessionStore } from '../claude-session-store.js';
import type { SessionStoreEntry } from '@anthropic-ai/claude-agent-sdk';

/**
 * claude-sdk-sessionstore #9 (Part 1) — PostgresSessionStore tests.
 *
 * DB-opt-in (DATABASE_URL), mirrors checkpoints.test.ts: skips cleanly when the
 * var is unset; otherwise applies the server + coder schemas and exercises the
 * real append/load/listSessions/delete/listSubkeys round trips against postgres.
 * Rows are namespaced under a unique project_key so concurrent suites / leftover
 * data can't collide, and afterAll deletes everything written.
 */
describe.runIf(!!process.env.DATABASE_URL)('PostgresSessionStore (DB)', () => {
  let sql: ReturnType<typeof postgres>;
  let store: PostgresSessionStore;
  const projectKey = `claude-store-test-${Date.now()}`;

  const entry = (type: string, extra: Record<string, unknown> = {}): SessionStoreEntry => ({
    type,
    ...extra,
  });

  beforeAll(async () => {
    sql = postgres(process.env.DATABASE_URL!, { max: 3 });
    const serverSchema = resolve(__dirname, '../../../../../server/src/schema.sql');
    const coderSchema = resolve(__dirname, '../../../schema.sql');
    await sql.unsafe(readFileSync(serverSchema, 'utf8'));
    await sql.unsafe(readFileSync(coderSchema, 'utf8'));
    store = new PostgresSessionStore(sql);
  });

  afterAll(async () => {
    if (sql) {
      await sql`DELETE FROM claude_session_entries WHERE project_key = ${projectKey}`.catch(() => {});
      await sql.end({ timeout: 5 });
    }
  });

  it('append → load round-trips and preserves order across two appends', async () => {
    const key = { projectKey, sessionId: 'sess-order' };
    await store.append(key, [entry('user', { uuid: 'u1' }), entry('assistant', { uuid: 'a1' })]);
    await store.append(key, [entry('result', { uuid: 'r1' })]);

    const loaded = await store.load(key);
    expect(loaded).not.toBeNull();
    expect(loaded!.map((e) => e.uuid)).toEqual(['u1', 'a1', 'r1']);
    expect(loaded!.map((e) => e.type)).toEqual(['user', 'assistant', 'result']);
  });

  it('append with an empty batch is a no-op (load still null for an otherwise-unseen key)', async () => {
    const key = { projectKey, sessionId: 'sess-empty' };
    await store.append(key, []);
    expect(await store.load(key)).toBeNull();
  });

  it('load of a key that was never written returns null', async () => {
    expect(await store.load({ projectKey, sessionId: 'never-seen' })).toBeNull();
  });

  it('isolates the main transcript from a subpath (load each independently)', async () => {
    const sessionId = 'sess-subpath';
    const mainKey = { projectKey, sessionId };
    const subKey = { projectKey, sessionId, subpath: 'subagents/x' };

    await store.append(mainKey, [entry('user', { uuid: 'main-1' })]);
    await store.append(subKey, [entry('assistant', { uuid: 'sub-1' })]);

    const main = await store.load(mainKey);
    const sub = await store.load(subKey);
    expect(main!.map((e) => e.uuid)).toEqual(['main-1']);
    expect(sub!.map((e) => e.uuid)).toEqual(['sub-1']);
  });

  it('listSessions returns the session with a numeric mtime (main transcripts only)', async () => {
    const sessionId = 'sess-list';
    await store.append({ projectKey, sessionId }, [entry('user', { uuid: 'l1' })]);
    // A subagent-only session must NOT surface as a main-transcript session.
    await store.append(
      { projectKey, sessionId: 'sess-sub-only', subpath: 'subagents/y' },
      [entry('user', { uuid: 's1' })],
    );

    const sessions = await store.listSessions(projectKey);
    const ids = sessions.map((s) => s.sessionId);
    expect(ids).toContain(sessionId);
    expect(ids).not.toContain('sess-sub-only');

    const row = sessions.find((s) => s.sessionId === sessionId)!;
    expect(typeof row.mtime).toBe('number');
    expect(Number.isFinite(row.mtime)).toBe(true);
    expect(row.mtime).toBeGreaterThan(0);
  });

  it('delete with a subpath removes only that subpath', async () => {
    const sessionId = 'sess-del-subpath';
    const mainKey = { projectKey, sessionId };
    const subKey = { projectKey, sessionId, subpath: 'subagents/z' };
    await store.append(mainKey, [entry('user', { uuid: 'keep-1' })]);
    await store.append(subKey, [entry('assistant', { uuid: 'drop-1' })]);

    await store.delete(subKey);

    expect(await store.load(subKey)).toBeNull();
    expect((await store.load(mainKey))!.map((e) => e.uuid)).toEqual(['keep-1']);
  });

  it('delete without a subpath removes the whole session (all subpaths)', async () => {
    const sessionId = 'sess-del-all';
    const mainKey = { projectKey, sessionId };
    const subKey = { projectKey, sessionId, subpath: 'subagents/w' };
    await store.append(mainKey, [entry('user', { uuid: 'm' })]);
    await store.append(subKey, [entry('assistant', { uuid: 's' })]);

    await store.delete({ projectKey, sessionId });

    expect(await store.load(mainKey)).toBeNull();
    expect(await store.load(subKey)).toBeNull();
    expect(await store.listSubkeys({ projectKey, sessionId })).toEqual([]);
  });

  it('listSubkeys returns the distinct non-main subpaths', async () => {
    const sessionId = 'sess-subkeys';
    await store.append({ projectKey, sessionId }, [entry('user', { uuid: 'main' })]);
    await store.append({ projectKey, sessionId, subpath: 'subagents/a' }, [entry('user', { uuid: 'a1' })]);
    await store.append({ projectKey, sessionId, subpath: 'subagents/a' }, [entry('user', { uuid: 'a2' })]);
    await store.append({ projectKey, sessionId, subpath: 'subagents/b' }, [entry('user', { uuid: 'b1' })]);

    const subkeys = await store.listSubkeys({ projectKey, sessionId });
    expect(subkeys.sort()).toEqual(['subagents/a', 'subagents/b']);
  });
});
