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

/* ── Mock logger ─────────────────────────────────────────────────── */

const mockError = vi.hoisted(() => vi.fn());
vi.mock("./logger.js", () => {
  const noop = () => {};
  const noopLogger = {
    info: noop,
    warn: noop,
    error: mockError,
    debug: noop,
    trace: noop,
    fatal: noop,
    child: () => noopLogger,
  };
  return {
    logger: noopLogger,
    createLogger: () => noopLogger,
    createSessionLogger: () => noopLogger,
  };
});

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

import {
  AppError,
  ErrorCode,
  notFound,
  validationError,
  unauthorized,
  conflict,
  internalError,
  errorHandler,
} from "./errors.js";

/* ── AppError class ──────────────────────────────────────────────── */

describe("AppError", () => {
  it("sets all properties correctly", () => {
    const err = new AppError(ErrorCode.SESSION_NOT_FOUND, "gone", 404, { id: "abc" });
    expect(err).toBeInstanceOf(Error);
    expect(err).toBeInstanceOf(AppError);
    expect(err.name).toBe("AppError");
    expect(err.code).toBe(ErrorCode.SESSION_NOT_FOUND);
    expect(err.message).toBe("gone");
    expect(err.statusCode).toBe(404);
    expect(err.details).toEqual({ id: "abc" });
  });

  it("defaults statusCode to 500", () => {
    const err = new AppError(ErrorCode.INTERNAL_ERROR, "oops");
    expect(err.statusCode).toBe(500);
  });
});

/* ── Factory functions ───────────────────────────────────────────── */

describe("factory functions", () => {
  it("notFound creates 404 with default code", () => {
    const err = notFound("Item missing");
    expect(err.statusCode).toBe(404);
    expect(err.code).toBe(ErrorCode.NOT_FOUND);
    expect(err.message).toBe("Item missing");
  });

  it("notFound accepts a custom error code", () => {
    const err = notFound("Session missing", ErrorCode.SESSION_NOT_FOUND);
    expect(err.code).toBe(ErrorCode.SESSION_NOT_FOUND);
    expect(err.statusCode).toBe(404);
  });

  it("validationError creates 400 with details", () => {
    const err = validationError("Bad input", { field: "name" });
    expect(err.statusCode).toBe(400);
    expect(err.code).toBe(ErrorCode.VALIDATION_ERROR);
    expect(err.details).toEqual({ field: "name" });
  });

  it("unauthorized creates 401", () => {
    const err = unauthorized("Invalid token");
    expect(err.statusCode).toBe(401);
    expect(err.code).toBe(ErrorCode.AUTH_FAILED);
  });

  it("conflict creates 409", () => {
    const err = conflict("Already exists");
    expect(err.statusCode).toBe(409);
    expect(err.code).toBe(ErrorCode.CONFLICT);
  });

  it("internalError creates 500 with details", () => {
    const err = internalError("Something broke", { stack: "..." });
    expect(err.statusCode).toBe(500);
    expect(err.code).toBe(ErrorCode.INTERNAL_ERROR);
    expect(err.details).toEqual({ stack: "..." });
  });
});

/* ── Error handler middleware ────────────────────────────────────── */

describe("errorHandler", () => {
  function createApp(throwFn: () => never) {
    const app = new Hono();
    app.onError(errorHandler());
    app.get("/test", () => {
      throwFn();
    });
    return app;
  }

  it("returns structured JSON for AppError", async () => {
    const app = createApp(() => {
      throw notFound("Session not found", ErrorCode.SESSION_NOT_FOUND);
    });

    const res = await app.request("/test");
    expect(res.status).toBe(404);
    const body = await res.json();
    expect(body).toEqual({
      error: "Session not found",
      code: ErrorCode.SESSION_NOT_FOUND,
    });
  });

  it("includes details when present", async () => {
    const app = createApp(() => {
      throw validationError("Invalid field", { field: "email" });
    });

    const res = await app.request("/test");
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body).toEqual({
      error: "Invalid field",
      code: ErrorCode.VALIDATION_ERROR,
      details: { field: "email" },
    });
  });

  it("returns generic 500 for non-AppError", async () => {
    mockError.mockClear();
    const app = createApp(() => {
      throw new Error("unexpected boom");
    });

    const res = await app.request("/test");
    expect(res.status).toBe(500);
    const body = await res.json();
    expect(body).toEqual({
      error: "Internal server error",
      code: ErrorCode.INTERNAL_ERROR,
    });
    expect(mockError).toHaveBeenCalledWith(
      expect.objectContaining({ err: expect.any(Error) }),
      "Unhandled error",
    );
  });

  it("returns generic 500 for non-Error thrown value", async () => {
    // Hono wraps non-Error throws into an Error with the message being the thrown value.
    // Our handler still catches it as a generic Error and returns 500.
    const app = createApp(() => {
      throw new TypeError("type mismatch");
    });

    const res = await app.request("/test");
    expect(res.status).toBe(500);
    const body = await res.json();
    expect(body.code).toBe(ErrorCode.INTERNAL_ERROR);
  });

  it("preserves custom statusCode from AppError", async () => {
    const app = createApp(() => {
      throw new AppError(ErrorCode.RATE_LIMITED, "Too many requests", 429);
    });

    const res = await app.request("/test");
    expect(res.status).toBe(429);
    const body = await res.json();
    expect(body).toEqual({
      error: "Too many requests",
      code: ErrorCode.RATE_LIMITED,
    });
  });
});
