import { env, fetchMock } from "cloudflare:test";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import app from "../src/app";
import { activateTestSite, authHeaders, seedLicenseAndBucket } from "./helpers";

const SHA = "c".repeat(64);
const S3_ORIGIN = "https://s3.eu-central-1.wasabisys.com";

beforeAll(() => {
  fetchMock.activate();
  fetchMock.disableNetConnect();
});
afterEach(() => fetchMock.assertNoPendingInterceptors());

function confirm(token: string, sha256: string) {
  return app.request(
    "/v1/storage/blobs/confirm",
    { method: "POST", headers: authHeaders(token), body: JSON.stringify({ sha256 }) },
    env,
  );
}

async function seedPendingBlob(siteId: string, declaredSize: number) {
  await env.DB.prepare(
    "INSERT INTO blobs (site_id, sha256, size_bytes, status) VALUES (?, ?, ?, 'pending')",
  )
    .bind(siteId, SHA, declaredSize)
    .run();
}

describe("POST /v1/storage/blobs/confirm", () => {
  beforeEach(seedLicenseAndBucket);

  it("marks the blob stored and adds the real size to usage", async () => {
    const { site_id, site_token } = await activateTestSite();
    await seedPendingBlob(site_id, 1234);
    fetchMock
      .get(S3_ORIGIN)
      .intercept({ path: `/wpback-test-001/s/${site_id}/blobs/${SHA}`, method: "HEAD" })
      .reply(200, "", { headers: { "content-length": "1234" } });

    const res = await confirm(site_token, SHA);
    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({ status: "stored" });

    const site = await env.DB.prepare("SELECT storage_bytes_used FROM sites WHERE id = ?")
      .bind(site_id)
      .first<{ storage_bytes_used: number }>();
    expect(site?.storage_bytes_used).toBe(1234);
  });

  it("re-arms created_at when claiming a pending blob as stored", async () => {
    const { site_id, site_token } = await activateTestSite();
    await env.DB.prepare(
      "INSERT INTO blobs (site_id, sha256, size_bytes, status, created_at) VALUES (?, ?, 1234, 'pending', '2020-01-01T00:00:00Z')",
    )
      .bind(site_id, SHA)
      .run();
    fetchMock
      .get(S3_ORIGIN)
      .intercept({ path: `/wpback-test-001/s/${site_id}/blobs/${SHA}`, method: "HEAD" })
      .reply(200, "", { headers: { "content-length": "1234" } });

    const res = await confirm(site_token, SHA);
    expect(res.status).toBe(200);

    const row = await env.DB.prepare(
      "SELECT created_at FROM blobs WHERE site_id = ? AND sha256 = ?",
    )
      .bind(site_id, SHA)
      .first<{ created_at: string }>();
    expect(row?.created_at).not.toBe("2020-01-01T00:00:00Z");
  });

  it("is idempotent for an already stored blob (no second HEAD)", async () => {
    const { site_id, site_token } = await activateTestSite();
    await env.DB.prepare(
      "INSERT INTO blobs (site_id, sha256, size_bytes, status) VALUES (?, ?, 10, 'stored')",
    )
      .bind(site_id, SHA)
      .run();
    const res = await confirm(site_token, SHA);
    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({ status: "stored" });

    const site = await env.DB.prepare("SELECT storage_bytes_used FROM sites WHERE id = ?")
      .bind(site_id)
      .first<{ storage_bytes_used: number }>();
    expect(site?.storage_bytes_used).toBe(0);
  });

  it("returns 400 not_uploaded when the object is missing in Wasabi", async () => {
    const { site_id, site_token } = await activateTestSite();
    await seedPendingBlob(site_id, 1234);
    fetchMock
      .get(S3_ORIGIN)
      .intercept({ path: `/wpback-test-001/s/${site_id}/blobs/${SHA}`, method: "HEAD" })
      .reply(404, "");
    const res = await confirm(site_token, SHA);
    expect(res.status).toBe(400);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe("not_uploaded");
  });

  it("deletes the object and the row on size mismatch (409)", async () => {
    const { site_id, site_token } = await activateTestSite();
    await seedPendingBlob(site_id, 1234);
    const path = `/wpback-test-001/s/${site_id}/blobs/${SHA}`;
    fetchMock
      .get(S3_ORIGIN)
      .intercept({ path, method: "HEAD" })
      .reply(200, "", { headers: { "content-length": "999" } });
    fetchMock.get(S3_ORIGIN).intercept({ path, method: "DELETE" }).reply(204, "");

    const res = await confirm(site_token, SHA);
    expect(res.status).toBe(409);
    const blob = await env.DB.prepare(
      "SELECT sha256 FROM blobs WHERE site_id = ? AND sha256 = ?",
    )
      .bind(site_id, SHA)
      .first();
    expect(blob).toBeNull();

    const site = await env.DB.prepare("SELECT storage_bytes_used FROM sites WHERE id = ?")
      .bind(site_id)
      .first<{ storage_bytes_used: number }>();
    expect(site?.storage_bytes_used).toBe(0);
  });

  it("returns 404 for a blob the site never declared", async () => {
    const { site_token } = await activateTestSite();
    const res = await confirm(site_token, SHA);
    expect(res.status).toBe(404);
  });
});
