import { Hono } from "hono";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";

/* ── Mocks ────────────────────────────────────────────────────────── */

vi.mock("../logger.js", () => ({
  createLogger: () => ({
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  }),
}));

vi.mock("../fs-permissions.js", () => ({
  ensureSecureDir: vi.fn(),
  writeSecureFile: vi.fn(),
}));

/* ── Imports ─────────────────────────────────────────────────────── */

import { registerQueueRoutes } from "./queue-routes.js";
import { WsBridge } from "../ws-bridge.js";
import { SessionManager } from "../session-manager.js";
import { SessionStore } from "../session-store.js";
import { errorHandler } from "../errors.js";
import type { Session } from "../ws-bridge-types.js";
import type { QueuedMessage } from "../session-types.js";

/* ── Helpers ──────────────────────────────────────────────────────── */

function req(app: Hono, method: string, path: string, body?: unknown) {
  const init: RequestInit = { method, headers: {} };
  if (body) {
    (init.headers as Record<string, string>)["content-type"] = "application/json";
    init.body = JSON.stringify(body);
  }
  return app.request(path, init);
}

/* ── Test suite ───────────────────────────────────────────────────── */

describe("queue-routes", () => {
  let app: Hono;
  let sm: SessionManager;
  let bridge: WsBridge;
  let session: Session;

  beforeEach(() => {
    sm = new SessionManager();
    bridge = new WsBridge(sm);
    session = sm.getOrCreateSession("test-session");

    app = new Hono();
    app.onError(errorHandler());
    registerQueueRoutes(app, { wsBridge: bridge });
  });

  // ─── GET /sessions/:id/queue ──────────────────────────────────

  describe("GET /sessions/:id/queue", () => {
    it("returns empty queue for existing session", async () => {
      const res = await req(app, "GET", "/sessions/test-session/queue");
      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body).toEqual({ queue: [], paused: false });
    });

    it("returns 404 for non-existent session", async () => {
      const res = await req(app, "GET", "/sessions/no-such/queue");
      expect(res.status).toBe(404);
    });
  });

  // ─── POST /sessions/:id/queue ─────────────────────────────────

  describe("POST /sessions/:id/queue", () => {
    it("adds a message to the queue", async () => {
      const res = await req(app, "POST", "/sessions/test-session/queue", {
        content: "hello world",
      });
      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.ok).toBe(true);
      expect(body.queue).toHaveLength(1);
      expect(body.queue[0].content).toBe("hello world");
      expect(body.queue[0].id).toBeDefined();
      expect(body.queue[0].queuedAt).toBeGreaterThan(0);
    });

    it("returns 400 when queue is at cap", async () => {
      // Fill queue to cap
      for (let i = 0; i < WsBridge.QUEUE_CAP; i++) {
        bridge.addToQueue(session, `msg-${i}`);
      }

      const res = await req(app, "POST", "/sessions/test-session/queue", {
        content: "one too many",
      });
      expect(res.status).toBe(400);
      const body = await res.json();
      expect(body.error).toContain("full");
    });

    it("returns 400 for empty content", async () => {
      const res = await req(app, "POST", "/sessions/test-session/queue", {
        content: "",
      });
      expect(res.status).toBe(400);
    });

    it("returns 400 for missing content", async () => {
      const res = await req(app, "POST", "/sessions/test-session/queue", {});
      expect(res.status).toBe(400);
    });

    it("returns 404 for non-existent session", async () => {
      const res = await req(app, "POST", "/sessions/no-such/queue", {
        content: "hello",
      });
      expect(res.status).toBe(404);
    });
  });

  // ─── DELETE /sessions/:id/queue/:msgId ────────────────────────

  describe("DELETE /sessions/:id/queue/:msgId", () => {
    it("removes a message from the queue", async () => {
      bridge.addToQueue(session, "msg1");
      bridge.addToQueue(session, "msg2");
      const msgId = session.messageQueue[0].id;

      const res = await req(app, "DELETE", `/sessions/test-session/queue/${msgId}`);
      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.ok).toBe(true);
      expect(body.queue).toHaveLength(1);
      expect(body.queue[0].content).toBe("msg2");
    });

    it("returns 404 for non-existent message", async () => {
      const res = await req(app, "DELETE", "/sessions/test-session/queue/no-such-id");
      expect(res.status).toBe(404);
    });

    it("returns 404 for non-existent session", async () => {
      const res = await req(app, "DELETE", "/sessions/no-such/queue/some-id");
      expect(res.status).toBe(404);
    });
  });

  // ─── POST /sessions/:id/queue/clear ───────────────────────────

  describe("POST /sessions/:id/queue/clear", () => {
    it("clears all messages from the queue", async () => {
      bridge.addToQueue(session, "msg1");
      bridge.addToQueue(session, "msg2");

      const res = await req(app, "POST", "/sessions/test-session/queue/clear");
      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.ok).toBe(true);

      expect(session.messageQueue).toHaveLength(0);
    });

    it("returns 404 for non-existent session", async () => {
      const res = await req(app, "POST", "/sessions/no-such/queue/clear");
      expect(res.status).toBe(404);
    });
  });

  // ─── POST /sessions/:id/queue/resume ──────────────────────────

  describe("POST /sessions/:id/queue/resume", () => {
    it("resumes a paused queue", async () => {
      session.queuePaused = true;

      const res = await req(app, "POST", "/sessions/test-session/queue/resume");
      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.ok).toBe(true);

      expect(session.queuePaused).toBe(false);
    });

    it("returns 404 for non-existent session", async () => {
      const res = await req(app, "POST", "/sessions/no-such/queue/resume");
      expect(res.status).toBe(404);
    });
  });

  // ─── POST /sessions/:id/queue with images ────────────────────

  describe("POST /sessions/:id/queue with images", () => {
    it("adds a message with valid images field", async () => {
      const res = await req(app, "POST", "/sessions/test-session/queue", {
        content: "look at this",
        images: [{ media_type: "image/png", data: "base64data==" }],
      });
      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.ok).toBe(true);
      expect(body.queue).toHaveLength(1);
      expect(body.queue[0].content).toBe("look at this");
      expect(body.queue[0].images).toEqual([
        { media_type: "image/png", data: "base64data==" },
      ]);
    });

    it("returns 400 for images with missing media_type", async () => {
      const res = await req(app, "POST", "/sessions/test-session/queue", {
        content: "bad image",
        images: [{ data: "base64data==" }],
      });
      expect(res.status).toBe(400);
    });
  });

  // ─── POST /sessions/:id/queue/reorder ────────────────────────

  describe("POST /sessions/:id/queue/reorder", () => {
    it("reorders the queue with valid orderedIds", async () => {
      bridge.addToQueue(session, "first");
      bridge.addToQueue(session, "second");
      bridge.addToQueue(session, "third");
      const ids = session.messageQueue.map((m) => m.id);

      const res = await req(app, "POST", "/sessions/test-session/queue/reorder", {
        orderedIds: [ids[2], ids[0], ids[1]],
      });
      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.ok).toBe(true);
      expect(body.queue.map((m: QueuedMessage) => m.content)).toEqual([
        "third",
        "first",
        "second",
      ]);
    });

    it("returns 400 with invalid orderedIds", async () => {
      bridge.addToQueue(session, "msg1");
      const res = await req(app, "POST", "/sessions/test-session/queue/reorder", {
        orderedIds: ["nonexistent-id"],
      });
      expect(res.status).toBe(400);
    });

    it("returns 404 for non-existent session", async () => {
      const res = await req(app, "POST", "/sessions/no-such/queue/reorder", {
        orderedIds: ["a"],
      });
      expect(res.status).toBe(404);
    });
  });

  // ─── PATCH /sessions/:id/queue/:msgId ────────────────────────

  describe("PATCH /sessions/:id/queue/:msgId", () => {
    it("edits content of a queued message", async () => {
      bridge.addToQueue(session, "original");
      const msgId = session.messageQueue[0].id;

      const res = await req(app, "PATCH", `/sessions/test-session/queue/${msgId}`, {
        content: "updated",
      });
      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.ok).toBe(true);
      expect(body.queue[0].content).toBe("updated");
    });

    it("edits content and images", async () => {
      bridge.addToQueue(session, "original");
      const msgId = session.messageQueue[0].id;

      const res = await req(app, "PATCH", `/sessions/test-session/queue/${msgId}`, {
        content: "with image",
        images: [{ media_type: "image/jpeg", data: "jpegdata==" }],
      });
      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.ok).toBe(true);
      expect(body.queue[0].content).toBe("with image");
      expect(body.queue[0].images).toEqual([
        { media_type: "image/jpeg", data: "jpegdata==" },
      ]);
    });

    it("returns 404 for non-existent msgId", async () => {
      bridge.addToQueue(session, "msg");
      const res = await req(app, "PATCH", "/sessions/test-session/queue/no-such-id", {
        content: "updated",
      });
      expect(res.status).toBe(404);
    });

    it("returns 400 for empty content", async () => {
      bridge.addToQueue(session, "msg");
      const msgId = session.messageQueue[0].id;
      const res = await req(app, "PATCH", `/sessions/test-session/queue/${msgId}`, {
        content: "",
      });
      expect(res.status).toBe(400);
    });
  });
});

/* ── Queue processing unit tests (WsBridge) ──────────────────────── */

describe("WsBridge queue processing", () => {
  let sm: SessionManager;
  let bridge: WsBridge;
  let session: Session;

  beforeEach(() => {
    sm = new SessionManager();
    bridge = new WsBridge(sm);
    session = sm.getOrCreateSession("q-session");
  });

  describe("addToQueue", () => {
    it("adds a message with generated id", () => {
      const result = bridge.addToQueue(session, "test content");
      expect(result).toBe(true);
      expect(session.messageQueue).toHaveLength(1);
      expect(session.messageQueue[0].content).toBe("test content");
      expect(session.messageQueue[0].id).toBeDefined();
      expect(session.messageQueue[0].queuedAt).toBeGreaterThan(0);
    });

    it("enforces queue cap", () => {
      for (let i = 0; i < WsBridge.QUEUE_CAP; i++) {
        expect(bridge.addToQueue(session, `msg-${i}`)).toBe(true);
      }
      expect(bridge.addToQueue(session, "overflow")).toBe(false);
      expect(session.messageQueue).toHaveLength(WsBridge.QUEUE_CAP);
    });
  });

  describe("removeFromQueue", () => {
    it("removes a message by id", () => {
      bridge.addToQueue(session, "keep");
      bridge.addToQueue(session, "remove-me");
      const removeId = session.messageQueue[1].id;
      const result = bridge.removeFromQueue(session, removeId);
      expect(result).toBe(true);
      expect(session.messageQueue).toHaveLength(1);
      expect(session.messageQueue[0].content).toBe("keep");
    });

    it("returns false for non-existent id", () => {
      bridge.addToQueue(session, "msg");
      expect(bridge.removeFromQueue(session, "bad-id")).toBe(false);
    });
  });

  describe("clearQueue", () => {
    it("removes all messages", () => {
      bridge.addToQueue(session, "a");
      bridge.addToQueue(session, "b");
      bridge.clearQueue(session);
      expect(session.messageQueue).toHaveLength(0);
    });
  });

  describe("resumeQueue", () => {
    it("sets paused to false", () => {
      session.queuePaused = true;
      bridge.resumeQueue(session);
      expect(session.queuePaused).toBe(false);
    });
  });

  describe("getQueueState", () => {
    it("returns queue and paused state", () => {
      bridge.addToQueue(session, "hello");
      session.queuePaused = true;
      const state = bridge.getQueueState("q-session");
      expect(state).toEqual({
        queue: session.messageQueue,
        paused: true,
      });
    });

    it("returns null for non-existent session", () => {
      expect(bridge.getQueueState("no-such")).toBeNull();
    });
  });
});

/* ── Queue persistence tests ─────────────────────────────────────── */

describe("Queue persistence", () => {
  let tempDir: string;
  let store: SessionStore;
  let sm: SessionManager;
  let bridge: WsBridge;

  beforeEach(() => {
    tempDir = mkdtempSync(join(tmpdir(), "queue-test-"));
    store = new SessionStore(tempDir);
    sm = new SessionManager();
    sm.setStore(store);
    bridge = new WsBridge(sm);
  });

  afterEach(() => {
    rmSync(tempDir, { recursive: true, force: true });
  });

  it("persists queue state through save/load", () => {
    const session = sm.getOrCreateSession("persist-queue");
    bridge.addToQueue(session, "persisted msg");
    session.queuePaused = true;

    // Force sync persist
    sm.persistSession(session);
    store.flushAll();

    // saveSync and flushAll are mocked (fs-permissions mocked),
    // so directly test the PersistedSession shape via the store's load call
    // Since fs-permissions is mocked, we test indirectly via session state
    expect(session.messageQueue).toHaveLength(1);
    expect(session.messageQueue[0].content).toBe("persisted msg");
    expect(session.queuePaused).toBe(true);
  });

  it("defaults to empty queue when loading old sessions without queue", () => {
    // SessionManager.restoreFromDisk defaults these from PersistedSession
    // Test the fallback logic by restoring a session with no queue fields
    const session = sm.getOrCreateSession("old-session");
    // Fresh session should have empty queue by default
    expect(session.messageQueue).toEqual([]);
    expect(session.queuePaused).toBe(false);
  });
});
