import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from "vitest";
import request from "supertest";
import { mkdtemp, rm } from "fs/promises";
import { tmpdir } from "os";
import * as path from "path";

// runPublishJob does real git/Gitea/Discord work -- mocked here so these
// tests exercise route wiring and jobStore integration, not the full
// pipeline (that's proven separately via the live local smoke test, and
// unit-tested per-module elsewhere).
vi.mock("./publishEvent", () => ({
  runPublishJob: vi.fn(async (job: { requestId: string }) => {
    const { updateJob } = await import("./jobStore");
    await updateJob(job.requestId, {
      status: "pr_created",
      branchName: `event/${job.requestId}`,
      pullRequestUrl: "https://git.example/pulls/1",
    });
  }),
}));

let jobsDir: string;

// auth.ts reads UPSTREAM_API_SECRET (and NODE_ENV, for the fail-closed
// check) once at module load time, so every group below that needs a
// different combination calls this to get a guaranteed-fresh import
// reflecting whatever env is set right before calling it.
async function freshApp() {
  vi.resetModules();
  const { buildApp } = await import("./server");
  return buildApp();
}

beforeAll(async () => {
  jobsDir = await mkdtemp(path.join(tmpdir(), "server-test-jobs-"));
});

afterAll(async () => {
  await rm(jobsDir, { recursive: true, force: true });
});

beforeEach(() => {
  process.env.JOBS_DIR = jobsDir;
  process.env.NODE_ENV = "test"; // not "production" -> dev-uploads route mounts
  delete process.env.UPSTREAM_API_SECRET;
});

describe("GET /health", () => {
  it("responds ok with no auth required", async () => {
    const res = await request(await freshApp()).get("/health");
    expect(res.status).toBe(200);
    expect(res.body).toEqual({ status: "ok" });
  });
});

describe("auth enforcement on /api/events routes", () => {
  it("lets requests through with no secret configured (dev default)", async () => {
    const res = await request(await freshApp()).get("/api/events/anything");
    expect(res.status).not.toBe(401);
  });

  it("rejects a request with no bearer token once UPSTREAM_API_SECRET is set", async () => {
    process.env.UPSTREAM_API_SECRET = "s3cret";
    const res = await request(await freshApp()).get("/api/events/anything");
    expect(res.status).toBe(401);
  });

  it("rejects a request with the wrong bearer token", async () => {
    process.env.UPSTREAM_API_SECRET = "s3cret";
    const res = await request(await freshApp()).get("/api/events/anything").set("Authorization", "Bearer wrong");
    expect(res.status).toBe(401);
  });

  it("accepts a request with the correct bearer token", async () => {
    process.env.UPSTREAM_API_SECRET = "s3cret";
    const res = await request(await freshApp()).get("/api/events/anything").set("Authorization", "Bearer s3cret");
    expect(res.status).toBe(404); // past auth, 404 because that requestId doesn't exist -- not 401
  });

  it("refuses to build the app at all in production with no secret configured", async () => {
    process.env.NODE_ENV = "production";
    await expect(freshApp()).rejects.toThrow(/refusing to start/i);
  });
});

describe("core publish flow (shared job state across this block)", () => {
  const validBody = {
    requestId: "srv-test-1",
    submittedByEmail: "a@uwaterloo.ca",
    event: {
      name: "Server Test Event",
      short: "short",
      startDate: "August 05 2026 10:00",
      online: false,
      location: "DC 1351",
      descriptionMarkdown: "desc",
      year: 2026,
      term: "spring",
      slug: "server-test-event",
      poster: null,
    },
  };

  let app: import("express").Express;

  beforeAll(async () => {
    // Explicit, not inherited from the outer beforeEach -- this beforeAll
    // runs before that hook does, so it can't rely on it having already
    // cleaned up whatever env state the previous describe block's last
    // test left behind (e.g. NODE_ENV=production with no secret, which
    // would make freshApp() throw here instead of building normally).
    process.env.JOBS_DIR = jobsDir;
    process.env.NODE_ENV = "test";
    delete process.env.UPSTREAM_API_SECRET;
    app = await freshApp();
  });

  it("rejects a malformed payload with a 400 before ever touching the job store", async () => {
    const res = await request(app)
      .post("/api/events/publish")
      .send({ ...validBody, event: { ...validBody.event, name: "" } });
    expect(res.status).toBe(400);
    expect(res.body.error.code).toBe("VALIDATION_ERROR");
  });

  it("accepts a valid payload, runs the (mocked) publish job, and returns its outcome", async () => {
    const res = await request(app).post("/api/events/publish").send(validBody);
    expect(res.status).toBe(201);
    expect(res.body.status).toBe("pr_created");
    expect(res.body.pullRequestUrl).toBe("https://git.example/pulls/1");
  });

  it("is idempotent -- resubmitting the identical request returns the same result without re-running the job", async () => {
    const publishEvent = await import("./publishEvent");
    const before = (publishEvent.runPublishJob as any).mock.calls.length;

    const res = await request(app).post("/api/events/publish").send(validBody);
    expect(res.status).toBe(201);
    expect((publishEvent.runPublishJob as any).mock.calls.length).toBe(before); // not called again
  });

  it("returns 409 when the same requestId is reused with a different body", async () => {
    const res = await request(app)
      .post("/api/events/publish")
      .send({ ...validBody, submittedByEmail: "different@uwaterloo.ca" });
    expect(res.status).toBe(409);
  });

  it("GET /api/events/:requestId returns 404 for an unknown id", async () => {
    const res = await request(app).get("/api/events/does-not-exist");
    expect(res.status).toBe(404);
  });

  it("GET /api/events/:requestId returns the job for a known id", async () => {
    const res = await request(app).get(`/api/events/${validBody.requestId}`);
    expect(res.status).toBe(200);
    expect(res.body.status).toBe("pr_created");
  });
});

describe("POST /api/dev-uploads (non-production only)", () => {
  it("rejects a request with no file", async () => {
    const res = await request(await freshApp()).post("/api/dev-uploads");
    expect(res.status).toBe(400);
    expect(res.body.error.code).toBe("MISSING_FILE");
  });

  it("accepts a png and returns a safe generated filename, not the original", async () => {
    const res = await request(await freshApp())
      .post("/api/dev-uploads")
      .attach("poster", Buffer.from([0x89, 0x50, 0x4e, 0x47]), {
        filename: "my-original-name.png",
        contentType: "image/png",
      });
    expect(res.status).toBe(200);
    expect(res.body.filename).not.toContain("my-original-name");
    expect(res.body.sourceUrl).toContain(res.body.filename);
  });

  it("is not mounted at all when NODE_ENV=production", async () => {
    process.env.NODE_ENV = "production";
    process.env.UPSTREAM_API_SECRET = "s3cret"; // required to build the app at all in production
    const res = await request(await freshApp())
      .post("/api/dev-uploads")
      .attach("poster", Buffer.from([0x89, 0x50, 0x4e, 0x47]), { filename: "x.png", contentType: "image/png" });
    expect(res.status).toBe(404); // route doesn't exist at all, not just unauthorized
  });
});
