/**
 * Unit tests for SessionDb — in-memory SQLite.
 */
import { describe, it, expect, beforeEach } from "bun:test";
import { Database } from "bun:sqlite";

const SESSION_SCHEMA = `
CREATE TABLE IF NOT EXISTS agent_sessions (
  project_id TEXT NOT NULL, session_id TEXT PRIMARY KEY, provider TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'idle', cost_usd REAL NOT NULL DEFAULT 0,
  input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0,
  model TEXT NOT NULL DEFAULT '', error TEXT,
  created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_agent_sessions_project ON agent_sessions(project_id);
CREATE TABLE IF NOT EXISTS agent_messages (
  session_id TEXT NOT NULL, msg_id TEXT NOT NULL,
  role TEXT NOT NULL, content TEXT NOT NULL DEFAULT '',
  tool_name TEXT, tool_input TEXT,
  timestamp INTEGER NOT NULL, cost_usd REAL NOT NULL DEFAULT 0,
  input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (session_id, msg_id),
  FOREIGN KEY (session_id) REFERENCES agent_sessions(session_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_agent_messages_session ON agent_messages(session_id, timestamp);
`;

function createDb(): Database {
  const db = new Database(":memory:");
  db.exec("PRAGMA foreign_keys = ON");
  db.exec(SESSION_SCHEMA);
  return db;
}

function insertSession(db: Database, projectId: string, sessionId: string, updatedAt: number) {
  db.query(
    `INSERT INTO agent_sessions
       (project_id, session_id, provider, status, cost_usd, input_tokens, output_tokens, model, created_at, updated_at)
     VALUES (?1, ?2, 'claude', 'idle', 0, 0, 0, 'opus', ?3, ?4)`
  ).run(projectId, sessionId, updatedAt, updatedAt);
}

function insertMessage(db: Database, sessionId: string, msgId: string, ts: number) {
  db.query(
    `INSERT INTO agent_messages
       (session_id, msg_id, role, content, timestamp, cost_usd, input_tokens, output_tokens)
     VALUES (?1, ?2, 'assistant', 'hello', ?3, 0, 0, 0)`
  ).run(sessionId, msgId, ts);
}

describe("SessionDb", () => {
  let db: Database;

  beforeEach(() => {
    db = createDb();
  });

  describe("saveSession / loadSession", () => {
    it("round-trips a session", () => {
      insertSession(db, "proj-1", "sess-1", 1000);
      const row = db.query<{ session_id: string; project_id: string }, [string]>(
        "SELECT * FROM agent_sessions WHERE project_id = ? ORDER BY updated_at DESC LIMIT 1"
      ).get("proj-1");
      expect(row!.session_id).toBe("sess-1");
    });

    it("returns null for missing project", () => {
      const row = db.query<{ session_id: string }, [string]>(
        "SELECT * FROM agent_sessions WHERE project_id = ? LIMIT 1"
      ).get("nonexistent");
      expect(row).toBeNull();
    });

    it("upserts on conflict", () => {
      insertSession(db, "p", "s1", 100);
      db.query(
        `INSERT INTO agent_sessions
           (project_id, session_id, provider, status, cost_usd, input_tokens, output_tokens, model, created_at, updated_at)
         VALUES ('p', 's1', 'claude', 'done', 1.5, 100, 200, 'opus', 100, 200)
         ON CONFLICT(session_id) DO UPDATE SET status = excluded.status, cost_usd = excluded.cost_usd, updated_at = excluded.updated_at`
      ).run();
      const row = db.query<{ status: string; cost_usd: number }, [string]>(
        "SELECT status, cost_usd FROM agent_sessions WHERE session_id = ?"
      ).get("s1");
      expect(row!.status).toBe("done");
      expect(row!.cost_usd).toBe(1.5);
    });
  });

  describe("saveMessage / loadMessages", () => {
    it("stores and retrieves messages in order", () => {
      insertSession(db, "p", "s1", 100);
      insertMessage(db, "s1", "m1", 10);
      insertMessage(db, "s1", "m2", 20);
      insertMessage(db, "s1", "m3", 5);

      const rows = db.query<{ msg_id: string }, [string]>(
        "SELECT msg_id FROM agent_messages WHERE session_id = ? ORDER BY timestamp ASC"
      ).all("s1");
      expect(rows.map((r) => r.msg_id)).toEqual(["m3", "m1", "m2"]);
    });

    it("saveMessages batch inserts via transaction", () => {
      insertSession(db, "p", "s1", 100);
      const stmt = db.prepare(
        `INSERT INTO agent_messages (session_id, msg_id, role, content, timestamp, cost_usd, input_tokens, output_tokens)
         VALUES (?1, ?2, 'user', 'text', ?3, 0, 0, 0) ON CONFLICT DO NOTHING`
      );
      const tx = db.transaction(() => {
        for (let i = 0; i < 5; i++) stmt.run("s1", `msg-${i}`, i * 10);
      });
      tx();

      const count = db.query<{ cnt: number }, [string]>(
        "SELECT COUNT(*) as cnt FROM agent_messages WHERE session_id = ?"
      ).get("s1");
      expect(count!.cnt).toBe(5);
    });

    it("ON CONFLICT DO NOTHING skips duplicates", () => {
      insertSession(db, "p", "s1", 100);
      insertMessage(db, "s1", "m1", 10);
      // Insert same msg_id again — should not throw
      db.query(
        `INSERT INTO agent_messages (session_id, msg_id, role, content, timestamp, cost_usd, input_tokens, output_tokens)
         VALUES ('s1', 'm1', 'user', 'different', 20, 0, 0, 0) ON CONFLICT(session_id, msg_id) DO NOTHING`
      ).run();

      const row = db.query<{ content: string }, [string, string]>(
        "SELECT content FROM agent_messages WHERE session_id = ? AND msg_id = ?"
      ).get("s1", "m1");
      expect(row!.content).toBe("hello"); // original, not overwritten
    });
  });

  describe("listSessionsByProject", () => {
    it("returns sessions ordered by updated_at DESC, limit 20", () => {
      for (let i = 0; i < 25; i++) {
        insertSession(db, "proj", `s-${i}`, i);
      }
      const rows = db.query<{ session_id: string }, [string]>(
        "SELECT session_id FROM agent_sessions WHERE project_id = ? ORDER BY updated_at DESC LIMIT 20"
      ).all("proj");
      expect(rows).toHaveLength(20);
      expect(rows[0].session_id).toBe("s-24"); // most recent
    });
  });

  describe("pruneOldSessions", () => {
    it("keeps only keepCount most recent sessions", () => {
      for (let i = 0; i < 5; i++) {
        insertSession(db, "proj", `s-${i}`, i * 100);
      }

      db.query(
        `DELETE FROM agent_sessions WHERE project_id = ?1
         AND session_id NOT IN (
           SELECT session_id FROM agent_sessions WHERE project_id = ?1
           ORDER BY updated_at DESC LIMIT ?2
         )`
      ).run("proj", 2);

      const remaining = db.query<{ session_id: string }, [string]>(
        "SELECT session_id FROM agent_sessions WHERE project_id = ? ORDER BY updated_at DESC"
      ).all("proj");
      expect(remaining).toHaveLength(2);
      expect(remaining[0].session_id).toBe("s-4");
      expect(remaining[1].session_id).toBe("s-3");
    });

    it("cascades message deletion with foreign key", () => {
      insertSession(db, "proj", "old", 1);
      insertSession(db, "proj", "new", 1000);
      insertMessage(db, "old", "m1", 1);

      db.query(
        `DELETE FROM agent_sessions WHERE project_id = ?1
         AND session_id NOT IN (
           SELECT session_id FROM agent_sessions WHERE project_id = ?1
           ORDER BY updated_at DESC LIMIT ?2
         )`
      ).run("proj", 1);

      const msgs = db.query<{ msg_id: string }, [string]>(
        "SELECT msg_id FROM agent_messages WHERE session_id = ?"
      ).all("old");
      expect(msgs).toHaveLength(0);
    });
  });
});
