import express from "express";
import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { errorHandler } from "../middleware/index.js";
import { contentJobRoutes } from "../routes/content-jobs.js";

// ---------------------------------------------------------------------------
// Hoisted mocks
// ---------------------------------------------------------------------------

const { createMock, getByIdMock, listByCompanyMock, dispatchMock } = vi.hoisted(() => ({
  createMock: vi.fn(),
  getByIdMock: vi.fn(),
  listByCompanyMock: vi.fn(),
  dispatchMock: vi.fn(),
}));

vi.mock("../services/content-job-store.js", () => ({
  contentJobStore: vi.fn(() => ({
    create: createMock,
    getById: getByIdMock,
    listByCompany: listByCompanyMock,
    transition: vi.fn(),
  })),
}));

vi.mock("../services/content-job-runner.js", () => ({
  contentJobRunner: {
    dispatch: dispatchMock,
  },
}));

vi.mock("../services/live-events.js", () => ({
  subscribeCompanyLiveEvents: vi.fn(() => vi.fn()),
}));

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function makeJob(overrides: Record<string, unknown> = {}) {
  return {
    id: "job-1",
    companyId: "company-1",
    jobType: "test",
    status: "queued",
    input: {},
    sourceTaskId: null,
    resultAssetId: null,
    errorMessage: null,
    startedAt: null,
    finishedAt: null,
    createdAt: new Date("2026-01-01T00:00:00.000Z"),
    updatedAt: new Date("2026-01-01T00:00:00.000Z"),
    ...overrides,
  };
}

function createApp() {
  const app = express();
  app.use(express.json());
  app.use((req, _res, next) => {
    (req as any).actor = {
      type: "board",
      source: "local_implicit",
      userId: "user-1",
      companyIds: ["company-1"],
    };
    next();
  });
  app.use("/api", contentJobRoutes({} as any, {} as any));
  app.use(errorHandler);
  return app;
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("content-jobs routes", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe("POST /api/companies/:id/content-jobs", () => {
    it("returns 202 with jobId, status queued, and createdAt", async () => {
      const job = makeJob();
      createMock.mockResolvedValue(job);
      dispatchMock.mockReturnValue(undefined);

      const res = await request(createApp())
        .post("/api/companies/company-1/content-jobs")
        .send({ jobType: "test" });

      expect(res.status).toBe(202);
      expect(res.body.jobId).toBe("job-1");
      expect(res.body.status).toBe("queued");
      expect(res.body.createdAt).toBeDefined();
    });

    it("returns 400 when jobType is missing", async () => {
      const res = await request(createApp())
        .post("/api/companies/company-1/content-jobs")
        .send({});

      expect(res.status).toBe(400);
      expect(res.body.error).toMatch(/jobType/i);
    });

    it("returns 400 when jobType is an empty string", async () => {
      const res = await request(createApp())
        .post("/api/companies/company-1/content-jobs")
        .send({ jobType: "   " });

      expect(res.status).toBe(400);
      expect(res.body.error).toMatch(/jobType/i);
    });

    it("persists sourceTaskId on the created job and returns it via GET", async () => {
      const job = makeJob({ sourceTaskId: "task-abc-123" });
      createMock.mockResolvedValue(job);
      getByIdMock.mockResolvedValue(job);
      dispatchMock.mockReturnValue(undefined);

      const postRes = await request(createApp())
        .post("/api/companies/company-1/content-jobs")
        .send({ jobType: "test", sourceTaskId: "task-abc-123" });

      expect(postRes.status).toBe(202);
      expect(createMock).toHaveBeenCalledWith(
        "company-1",
        expect.objectContaining({ sourceTaskId: "task-abc-123" }),
      );

      const getRes = await request(createApp())
        .get(`/api/companies/company-1/content-jobs/job-1`);

      expect(getRes.status).toBe(200);
      expect(getRes.body.sourceTaskId).toBe("task-abc-123");
    });

    it("calls contentJobRunner.dispatch as fire-and-forget", async () => {
      createMock.mockResolvedValue(makeJob());
      dispatchMock.mockReturnValue(undefined);

      await request(createApp())
        .post("/api/companies/company-1/content-jobs")
        .send({ jobType: "render-slide" });

      expect(dispatchMock).toHaveBeenCalledTimes(1);
    });
  });

  describe("GET /api/companies/:id/content-jobs", () => {
    it("returns array of jobs ordered by createdAt desc", async () => {
      const jobs = [makeJob({ id: "job-2" }), makeJob({ id: "job-1" })];
      listByCompanyMock.mockResolvedValue(jobs);

      const res = await request(createApp())
        .get("/api/companies/company-1/content-jobs");

      expect(res.status).toBe(200);
      expect(Array.isArray(res.body)).toBe(true);
      expect(res.body.length).toBeGreaterThanOrEqual(1);
      expect(res.body[0].id).toBe("job-2");
    });
  });

  describe("GET /api/companies/:id/content-jobs/:jobId", () => {
    it("returns the job when found", async () => {
      getByIdMock.mockResolvedValue(makeJob());

      const res = await request(createApp())
        .get("/api/companies/company-1/content-jobs/job-1");

      expect(res.status).toBe(200);
      expect(res.body.id).toBe("job-1");
    });

    it("returns 404 when the job does not exist", async () => {
      getByIdMock.mockResolvedValue(null);

      const res = await request(createApp())
        .get("/api/companies/company-1/content-jobs/nonexistent-uuid");

      expect(res.status).toBe(404);
      expect(res.body.error).toBeDefined();
    });
  });
});
