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 MANIFEST_SHA = "e".repeat(64);
const S3_ORIGIN = "https://s3.eu-central-1.wasabisys.com";
const STATS = {
  files_total: 100,
  bytes_total: 5000,
  files_changed: 3,
  bytes_uploaded: 300,
  db_tables: 12,
};
const VALID_EMPTY_MANIFEST = {
  version: 1,
  ts: "2026-07-23T00:00:00Z",
  wp: {},
  files: [],
  db: [],
};

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

async function gzipBytes(value: unknown): Promise<Uint8Array> {
  const stream = new Response(JSON.stringify(value)).body!.pipeThrough(
    new CompressionStream("gzip"),
  );
  return new Uint8Array(await new Response(stream).arrayBuffer());
}

function commit(token: string, body: unknown) {
  return app.request(
    "/v1/snapshots/commit",
    { method: "POST", headers: authHeaders(token), body: JSON.stringify(body) },
    env,
  );
}

// Insère uniquement la row blob manifest `stored` (sans interceptor). Les tests
// qui veulent contrôler eux-mêmes la réponse GET (unreadable/invalid) l'utilisent.
function insertStoredManifestRow(siteId: string) {
  return env.DB.prepare(
    "INSERT INTO blobs (site_id, sha256, size_bytes, status) VALUES (?, ?, 50, 'stored')",
  )
    .bind(siteId, MANIFEST_SHA)
    .run();
}

// Enregistre la row blob manifest `stored` ET l'interceptor GET que le commit
// lit désormais côté serveur pour valider le contenu (Finding A2). Manifest vide
// = vacuously valide. `.persist()` couvre les tests qui committent plusieurs fois.
async function storeManifestBlob(
  siteId: string,
  manifest: unknown = VALID_EMPTY_MANIFEST,
) {
  await insertStoredManifestRow(siteId);
  fetchMock
    .get(S3_ORIGIN)
    .intercept({ path: `/wpback-test-001/s/${siteId}/blobs/${MANIFEST_SHA}`, method: "GET" })
    .reply(200, await gzipBytes(manifest))
    .persist();
}

describe("snapshots", () => {
  beforeEach(seedLicenseAndBucket);

  it("commits a snapshot whose manifest blob is stored", async () => {
    const { site_id, site_token } = await activateTestSite();
    await storeManifestBlob(site_id);
    const res = await commit(site_token, {
      ts: "2026-07-23T04:00:00Z",
      manifest_sha256: MANIFEST_SHA,
      stats: STATS,
    });
    expect(res.status).toBe(201);
    const body = (await res.json()) as { snapshot_id: number };
    expect(body.snapshot_id).toBeGreaterThan(0);
  });

  it("rejects a commit whose manifest is not a stored blob", async () => {
    const { site_token } = await activateTestSite();
    const res = await commit(site_token, {
      ts: "2026-07-23T04:00:00Z",
      manifest_sha256: MANIFEST_SHA,
      stats: STATS,
    });
    expect(res.status).toBe(400);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe("manifest_missing");
  });

  it("rejects non-UTC-ISO ts and malformed stats", async () => {
    // Rejeté avant toute logique de manifest : aucun interceptor à enregistrer.
    const { site_token } = await activateTestSite();
    expect(
      (
        await commit(site_token, {
          ts: "23/07/2026",
          manifest_sha256: MANIFEST_SHA,
          stats: STATS,
        })
      ).status,
    ).toBe(400);
    expect(
      (
        await commit(site_token, {
          ts: "2026-07-23T04:00:00Z",
          manifest_sha256: MANIFEST_SHA,
          stats: { files_total: -1 },
        })
      ).status,
    ).toBe(400);
  });

  it("rejects a ts carrying fractional seconds", async () => {
    // Les fractions cassent l'ordre lexicographique utilisé par le GC/listing :
    // rejet dès la regex, avant toute lecture manifest (pas d'interceptor).
    const { site_token } = await activateTestSite();
    const res = await commit(site_token, {
      ts: "2026-07-23T04:00:00.500Z",
      manifest_sha256: MANIFEST_SHA,
      stats: STATS,
    });
    expect(res.status).toBe(400);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe("invalid_request");
  });

  it("rejects a ts that is not a valid calendar date", async () => {
    // « 2026-13-01… » passe la regex mais Date.parse renvoie NaN. Le check doit
    // venir AVANT le clamp futur (NaN > x est false). Pas d'interceptor.
    const { site_token } = await activateTestSite();
    const res = await commit(site_token, {
      ts: "2026-13-01T00:00:00Z",
      manifest_sha256: MANIFEST_SHA,
      stats: STATS,
    });
    expect(res.status).toBe(400);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe("invalid_request");
  });

  it("rejects a commit whose manifest references a blob that is not stored", async () => {
    const { site_id, site_token } = await activateTestSite();
    const missingSha = "9".repeat(64);
    await storeManifestBlob(site_id, {
      version: 1,
      ts: "2026-07-23T00:00:00Z",
      wp: {},
      files: [{ p: "wp-content/x.php", h: missingSha, s: 100, m: 1721730000 }],
      db: [],
    });
    const res = await commit(site_token, {
      ts: "2026-07-23T04:00:00Z",
      manifest_sha256: MANIFEST_SHA,
      stats: STATS,
    });
    expect(res.status).toBe(400);
    const body = (await res.json()) as { error: { code: string; message: string } };
    expect(body.error.code).toBe("manifest_blobs_missing");
    expect(body.error.message).toContain("1");
  });

  it("rejects a commit whose manifest blob cannot be read from storage", async () => {
    const { site_id, site_token } = await activateTestSite();
    await insertStoredManifestRow(site_id);
    // Le GET renvoie 500. aws4fetch (retries=10, backoff exponentiel) réessaie ;
    // le 2e essai ne matche plus l'interceptor consommé et échoue via netConnect
    // désactivé → getObjectJson throw → manifest_unreadable. On n'enregistre
    // qu'un seul 500 (times défaut = 1) : rapide et sans interceptor pendant.
    fetchMock
      .get(S3_ORIGIN)
      .intercept({
        path: `/wpback-test-001/s/${site_id}/blobs/${MANIFEST_SHA}`,
        method: "GET",
      })
      .reply(500, "boom");
    const res = await commit(site_token, {
      ts: "2026-07-23T04:00:00Z",
      manifest_sha256: MANIFEST_SHA,
      stats: STATS,
    });
    expect(res.status).toBe(400);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe("manifest_unreadable");
  });

  it("rejects a commit whose manifest content is structurally invalid", async () => {
    const { site_id, site_token } = await activateTestSite();
    await insertStoredManifestRow(site_id);
    // gzip valide mais version inconnue → assertValidManifest throw → invalid.
    fetchMock
      .get(S3_ORIGIN)
      .intercept({
        path: `/wpback-test-001/s/${site_id}/blobs/${MANIFEST_SHA}`,
        method: "GET",
      })
      .reply(200, await gzipBytes({ version: 2, files: [], db: [] }));
    const res = await commit(site_token, {
      ts: "2026-07-23T04:00:00Z",
      manifest_sha256: MANIFEST_SHA,
      stats: STATS,
    });
    expect(res.status).toBe(400);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe("manifest_invalid");
  });

  it("rejects a commit whose ts is too far in the future", async () => {
    // Rejeté au check de skew avant toute logique de manifest : pas d'interceptor.
    const { site_token } = await activateTestSite();
    const future = new Date(Date.now() + 24 * 3600 * 1000).toISOString();
    const res = await commit(site_token, {
      ts: future,
      manifest_sha256: MANIFEST_SHA,
      stats: STATS,
    });
    expect(res.status).toBe(400);
    const body = (await res.json()) as { error: { code: string } };
    expect(body.error.code).toBe("invalid_request");
  });

  it("lists snapshots newest first", async () => {
    const { site_id, site_token } = await activateTestSite();
    await storeManifestBlob(site_id);
    await commit(site_token, {
      ts: "2026-07-22T04:00:00Z",
      manifest_sha256: MANIFEST_SHA,
      stats: STATS,
    });
    await commit(site_token, {
      ts: "2026-07-23T04:00:00Z",
      manifest_sha256: MANIFEST_SHA,
      stats: STATS,
    });
    const res = await app.request(
      "/v1/snapshots",
      { headers: authHeaders(site_token) },
      env,
    );
    expect(res.status).toBe(200);
    const body = (await res.json()) as {
      snapshots: { ts: string; stats: typeof STATS }[];
    };
    expect(body.snapshots.map((s) => s.ts)).toEqual([
      "2026-07-23T04:00:00Z",
      "2026-07-22T04:00:00Z",
    ]);
    expect(body.snapshots[0]?.stats).toEqual(STATS);
  });
});
