/**
 * Tests for the POST /sessions/batch-create endpoint.
 *
 * Validates:
 * - Successful batch creation of multiple sessions
 * - Partial failure handling (some succeed, some fail)
 * - Zod schema validation (empty array, too many sessions)
 * - Error response shape matches the contract
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import { Hono } from "hono";
import { z } from "zod";

// ─── Mock session-creator ────────────────────────────────────────────────────

const mockCreateSession = vi.hoisted(() => vi.fn());

vi.mock("./session-creator.js", () => ({
  createSession: mockCreateSession,
  SessionCreationError: class SessionCreationError extends Error {
    constructor(
      message: string,
      public readonly statusCode: number,
      public readonly step?: string,
    ) {
      super(message);
      this.name = "SessionCreationError";
    }
  },
}));

// Mock logger — prevents pino initialization issues in tests
vi.mock("./logger.js", () => {
  const noop = () => {};
  const noopLogger = {
    info: noop,
    warn: noop,
    error: noop,
    debug: noop,
    trace: noop,
    fatal: noop,
    child: () => noopLogger,
  };
  return {
    logger: noopLogger,
    createLogger: () => noopLogger,
    createSessionLogger: () => noopLogger,
  };
});

import { createSession, SessionCreationError } from "./session-creator.js";
import { parseBody } from "./validation.js";

// ─── Schema (mirrors the one in routes.ts) ───────────────────────────────────

const sessionCreateSchema = z.object({
  cwd: z.string().optional(),
  backend: z.enum(["claude", "codex"]).optional().default("claude"),
  model: z.string().max(200).optional(),
  prompt: z.string().optional(),
  resumeSessionAt: z.string().max(500).optional(),
  forkSession: z.boolean().optional(),
  envSlug: z.string().max(200).optional(),
  env: z.record(z.string(), z.string()).optional(),
  container: z.boolean().optional(),
  branch: z.string().max(500).optional(),
  createBranch: z.boolean().optional(),
  useWorktree: z.boolean().optional(),
  permissionMode: z.string().max(100).optional(),
  codexInternetAccess: z.boolean().optional(),
}).passthrough();

const batchCreateSchema = z.object({
  sessions: z.array(sessionCreateSchema).min(1).max(10),
});

// ─── Test app factory ────────────────────────────────────────────────────────

function createTestApp() {
  const app = new Hono();
  const sessionDeps = {} as any; // Deps are passed through to mock

  app.post("/sessions/batch-create", async (c) => {
    const parsed = await parseBody(c, batchCreateSchema);
    if (!parsed.success) return parsed.response;

    const results = await Promise.allSettled(
      parsed.data.sessions.map((sessionOpts: Record<string, unknown>) =>
        createSession(sessionOpts, sessionDeps),
      ),
    );

    const created: Array<{
      sessionId: string;
      state: string;
      cwd: string;
      backendType?: string;
    }> = [];
    const errors: Array<{ index: number; error: string }> = [];

    for (let i = 0; i < results.length; i++) {
      const result = results[i];
      if (result.status === "fulfilled") {
        created.push({
          sessionId: result.value.sessionId,
          state: result.value.state,
          cwd: result.value.cwd,
          backendType: result.value.backendType,
        });
      } else {
        const msg =
          result.reason instanceof SessionCreationError
            ? result.reason.message
            : result.reason instanceof Error
              ? result.reason.message
              : String(result.reason);
        errors.push({ index: i, error: msg });
      }
    }

    return c.json({
      created,
      errors,
      total: parsed.data.sessions.length,
    });
  });

  return app;
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

function makeRequest(app: Hono, body: unknown) {
  return app.request("/sessions/batch-create", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
}

// =============================================================================
// Tests
// =============================================================================

describe("POST /sessions/batch-create", () => {
  let app: Hono;

  beforeEach(() => {
    vi.clearAllMocks();
    app = createTestApp();
  });

  // ─── Success cases ──────────────────────────────────────────────────────

  describe("successful creation", () => {
    it("creates multiple sessions and returns created array", async () => {
      mockCreateSession
        .mockResolvedValueOnce({
          sessionId: "sess-1",
          state: "connected",
          cwd: "/test/a",
          backendType: "claude",
        })
        .mockResolvedValueOnce({
          sessionId: "sess-2",
          state: "connected",
          cwd: "/test/b",
          backendType: "codex",
        });

      const res = await makeRequest(app, {
        sessions: [
          { cwd: "/test/a", backend: "claude" },
          { cwd: "/test/b", backend: "codex" },
        ],
      });

      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.total).toBe(2);
      expect(body.created).toHaveLength(2);
      expect(body.errors).toHaveLength(0);
      expect(body.created[0]).toEqual({
        sessionId: "sess-1",
        state: "connected",
        cwd: "/test/a",
        backendType: "claude",
      });
      expect(body.created[1]).toEqual({
        sessionId: "sess-2",
        state: "connected",
        cwd: "/test/b",
        backendType: "codex",
      });
    });

    it("creates a single session in batch", async () => {
      mockCreateSession.mockResolvedValueOnce({
        sessionId: "sess-only",
        state: "connected",
        cwd: "/workspace",
        backendType: "claude",
      });

      const res = await makeRequest(app, {
        sessions: [{ cwd: "/workspace" }],
      });

      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.total).toBe(1);
      expect(body.created).toHaveLength(1);
      expect(body.errors).toHaveLength(0);
    });

    it("passes session options through to createSession", async () => {
      mockCreateSession.mockResolvedValue({
        sessionId: "sess-x",
        state: "connected",
        cwd: "/test",
        backendType: "claude",
      });

      await makeRequest(app, {
        sessions: [
          {
            cwd: "/test",
            backend: "claude",
            model: "sonnet",
            permissionMode: "plan",
            branch: "feature/test",
          },
        ],
      });

      expect(mockCreateSession).toHaveBeenCalledTimes(1);
      const passedOpts = mockCreateSession.mock.calls[0][0];
      expect(passedOpts.cwd).toBe("/test");
      expect(passedOpts.backend).toBe("claude");
      expect(passedOpts.model).toBe("sonnet");
      expect(passedOpts.permissionMode).toBe("plan");
      expect(passedOpts.branch).toBe("feature/test");
    });
  });

  // ─── Partial failure ────────────────────────────────────────────────────

  describe("partial failure", () => {
    it("returns created and errors when some sessions fail", async () => {
      mockCreateSession
        .mockResolvedValueOnce({
          sessionId: "sess-ok",
          state: "connected",
          cwd: "/ok",
          backendType: "claude",
        })
        .mockRejectedValueOnce(new Error("CLI not found"))
        .mockResolvedValueOnce({
          sessionId: "sess-ok2",
          state: "connected",
          cwd: "/ok2",
          backendType: "codex",
        });

      const res = await makeRequest(app, {
        sessions: [
          { cwd: "/ok" },
          { cwd: "/bad" },
          { cwd: "/ok2", backend: "codex" },
        ],
      });

      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.total).toBe(3);
      expect(body.created).toHaveLength(2);
      expect(body.errors).toHaveLength(1);
      expect(body.errors[0]).toEqual({
        index: 1,
        error: "CLI not found",
      });
    });

    it("handles SessionCreationError in partial failures", async () => {
      mockCreateSession
        .mockRejectedValueOnce(
          new SessionCreationError("Invalid backend: gemini", 400, "resolving_env"),
        )
        .mockResolvedValueOnce({
          sessionId: "sess-ok",
          state: "connected",
          cwd: "/ok",
          backendType: "claude",
        });

      const res = await makeRequest(app, {
        sessions: [
          { cwd: "/bad" },
          { cwd: "/ok" },
        ],
      });

      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.created).toHaveLength(1);
      expect(body.errors).toHaveLength(1);
      expect(body.errors[0].error).toBe("Invalid backend: gemini");
    });

    it("returns all errors when every session fails", async () => {
      mockCreateSession
        .mockRejectedValueOnce(new Error("fail 1"))
        .mockRejectedValueOnce(new Error("fail 2"));

      const res = await makeRequest(app, {
        sessions: [
          { cwd: "/a" },
          { cwd: "/b" },
        ],
      });

      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.total).toBe(2);
      expect(body.created).toHaveLength(0);
      expect(body.errors).toHaveLength(2);
      expect(body.errors[0]).toEqual({ index: 0, error: "fail 1" });
      expect(body.errors[1]).toEqual({ index: 1, error: "fail 2" });
    });
  });

  // ─── Validation ─────────────────────────────────────────────────────────

  describe("validation", () => {
    it("rejects empty sessions array", async () => {
      const res = await makeRequest(app, { sessions: [] });

      expect(res.status).toBe(400);
      const body = await res.json();
      expect(body.error).toBe("Validation failed");
    });

    it("rejects more than 10 sessions", async () => {
      const sessions = Array.from({ length: 11 }, (_, i) => ({
        cwd: `/test/${i}`,
      }));

      const res = await makeRequest(app, { sessions });

      expect(res.status).toBe(400);
      const body = await res.json();
      expect(body.error).toBe("Validation failed");
    });

    it("rejects missing sessions field", async () => {
      const res = await makeRequest(app, {});

      expect(res.status).toBe(400);
      const body = await res.json();
      expect(body.error).toBe("Validation failed");
    });

    it("rejects invalid JSON body", async () => {
      const res = await app.request("/sessions/batch-create", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: "not-json",
      });

      expect(res.status).toBe(400);
    });

    it("accepts exactly 10 sessions", async () => {
      mockCreateSession.mockResolvedValue({
        sessionId: "sess-x",
        state: "connected",
        cwd: "/test",
        backendType: "claude",
      });

      const sessions = Array.from({ length: 10 }, (_, i) => ({
        cwd: `/test/${i}`,
      }));

      const res = await makeRequest(app, { sessions });

      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.total).toBe(10);
      expect(body.created).toHaveLength(10);
    });

    it("accepts exactly 1 session (minimum)", async () => {
      mockCreateSession.mockResolvedValueOnce({
        sessionId: "sess-min",
        state: "connected",
        cwd: "/min",
        backendType: "claude",
      });

      const res = await makeRequest(app, {
        sessions: [{ cwd: "/min" }],
      });

      expect(res.status).toBe(200);
      const body = await res.json();
      expect(body.total).toBe(1);
    });
  });

  // ─── Response shape ─────────────────────────────────────────────────────

  describe("response shape", () => {
    it("always includes created, errors, and total fields", async () => {
      mockCreateSession.mockResolvedValueOnce({
        sessionId: "s1",
        state: "connected",
        cwd: "/test",
        backendType: "claude",
      });

      const res = await makeRequest(app, {
        sessions: [{ cwd: "/test" }],
      });

      const body = await res.json();
      expect(body).toHaveProperty("created");
      expect(body).toHaveProperty("errors");
      expect(body).toHaveProperty("total");
      expect(Array.isArray(body.created)).toBe(true);
      expect(Array.isArray(body.errors)).toBe(true);
      expect(typeof body.total).toBe("number");
    });

    it("created items have required fields", async () => {
      mockCreateSession.mockResolvedValueOnce({
        sessionId: "sess-shape",
        state: "running",
        cwd: "/workspace",
        backendType: "codex",
      });

      const res = await makeRequest(app, {
        sessions: [{ cwd: "/workspace", backend: "codex" }],
      });

      const body = await res.json();
      const item = body.created[0];
      expect(item).toHaveProperty("sessionId");
      expect(item).toHaveProperty("state");
      expect(item).toHaveProperty("cwd");
      expect(item).toHaveProperty("backendType");
    });
  });

  // ─── Concurrency ───────────────────────────────────────────────────────

  describe("concurrency", () => {
    it("creates sessions in parallel via Promise.allSettled", async () => {
      const callOrder: number[] = [];

      mockCreateSession.mockImplementation(async (opts: any) => {
        const index = parseInt(opts.cwd.split("/").pop(), 10);
        callOrder.push(index);
        // Simulate varying delays to verify parallelism
        await new Promise((r) => setTimeout(r, 10 - index));
        return {
          sessionId: `sess-${index}`,
          state: "connected",
          cwd: opts.cwd,
          backendType: "claude",
        };
      });

      const res = await makeRequest(app, {
        sessions: [
          { cwd: "/test/0" },
          { cwd: "/test/1" },
          { cwd: "/test/2" },
        ],
      });

      expect(res.status).toBe(200);
      const body = await res.json();
      // All three should be created regardless of completion order
      expect(body.created).toHaveLength(3);
      // createSession was called for each item
      expect(mockCreateSession).toHaveBeenCalledTimes(3);
    });
  });
});
