// @vitest-environment node
import { createHash } from "node:crypto";
import { readFileSync, readdirSync } from "node:fs";
import { expect, it } from "vitest";
import { ENVIRONMENTS } from "../src/content";

// 과거 ASR/보존 검사는 해당 버전의 정확한 바이트를 읽는다. 새 음원의 인수로 재사용하지 않는다.
const readHistoricalBytes = (path: string) =>
  readFileSync(
    path.startsWith("public/ambience/")
      ? path.replace(
          "public/ambience/",
          ".agent/evidence-v0.2.5/baseline/ambience/",
        )
      : path,
  );

// 생성기 자기 보고 대신 배포 바이트의 형식·파형·권리 추적 정보를 독립 측정한다.
it("여덟 현장 녹음은 고유 PCM16 WAV·짧은 fade·완전한 provenance/notice를 가진다", () => {
  const manifest = JSON.parse(
    readFileSync("public/ambience/manifest.json", "utf8"),
  );
  expect(manifest.version).toBe("0.2.5");
  expect(
    manifest.files.map((f: { environmentId: string }) => f.environmentId),
  ).toEqual(ENVIRONMENTS.map((e) => e.id));
  expect(readdirSync("public/ambience").sort()).toEqual(
    [
      ...ENVIRONMENTS.map((e) => `${e.id}.wav`),
      "manifest.json",
      "AMBIENCE_NOTICES.md",
    ].sort(),
  );
  const notice = readFileSync("public/ambience/AMBIENCE_NOTICES.md", "utf8");
  const hashes = new Set();
  for (const entry of manifest.files) {
    for (const key of [
      "displayName",
      "pageUrl",
      "directUrl",
      "creator",
      "license",
      "licenseUrl",
      "attribution",
      "conversion",
      "sourceSha256",
      "sha256",
      "bytes",
      "duration",
      "rms",
      "peak",
    ])
      expect(entry[key], key).toBeTruthy();
    expect(entry.sourceSha256).toMatch(/^[a-f0-9]{64}$/);
    expect(entry.license).toMatch(/CC0|Public domain|CC BY(?:-SA)? 4\.0/);
    const section = notice
      .split("## ")
      .find((s: string) =>
        s.startsWith(`${entry.displayName} — ${entry.environmentId}\n`),
      )!;
    expect(section).toContain(entry.attribution);
    for (const line of [
      `Creator: ${entry.creator}`,
      `Source: ${entry.pageUrl}`,
      `Download: ${entry.directUrl}`,
      `License: ${entry.license} — ${entry.licenseUrl}`,
      `Output license: ${entry.outputLicense}`,
      `Changes: ${entry.conversion.operations}`,
    ])
      expect(section).toContain(line);
    expect(notice).toContain(entry.licenseUrl);
    expect(notice).toContain(entry.pageUrl);
    const bytes = readFileSync(`public/ambience/${entry.file}`);
    const hash = createHash("sha256").update(bytes).digest("hex");
    hashes.add(hash);
    expect(hash).toBe(entry.sha256);
    expect(bytes.length).toBe(entry.bytes);
    expect(bytes.toString("ascii", 0, 4)).toBe("RIFF");
    expect(bytes.toString("ascii", 8, 12)).toBe("WAVE");
    expect(bytes.readUInt16LE(20)).toBe(1);
    expect(bytes.readUInt16LE(22)).toBe(1);
    expect(bytes.readUInt32LE(24)).toBe(24000);
    expect(bytes.readUInt16LE(34)).toBe(16);
    expect(bytes.readUInt32LE(40)).toBe(bytes.length - 44);
    const samples = Array.from(
      { length: (bytes.length - 44) / 2 },
      (_, i) => bytes.readInt16LE(44 + i * 2) / 32768,
    );
    const rms = Math.sqrt(
      samples.reduce((sum, n) => sum + n * n, 0) / samples.length,
    );
    const peak = samples.reduce((max, n) => Math.max(max, Math.abs(n)), 0);
    expect(rms).toBeCloseTo(entry.rms, 10);
    expect(peak).toBeCloseTo(entry.peak, 10);
    expect(rms).toBeGreaterThan(0.003);
    expect(peak).toBeLessThanOrEqual(0.85);
    expect(samples[0]).toBe(0);
    expect(samples.at(-1)).toBe(0);
    expect(entry.duration).toBe(samples.length / 24000);
    expect(entry.duration).toBeGreaterThanOrEqual(8);
    expect(entry.duration).toBeLessThanOrEqual(20);
    expect(entry.conversion.fadeSeconds).toBeGreaterThanOrEqual(0.025);
    expect(entry.conversion.startSeconds).toBeGreaterThanOrEqual(0);
  }
  expect(hashes.size).toBe(8);
});

it("원본 계획은 임시 경로 대신 검증된 해시·바이트·직접 미디어 길이를 고정한다", () => {
  const plan = JSON.parse(
    readFileSync(".agent/AMBIENCE-SOURCES-v0.2.3.json", "utf8"),
  );
  const manifest = JSON.parse(
    readFileSync("public/ambience/manifest.json", "utf8"),
  );
  const evidence = JSON.parse(
    readFileSync(".agent/evidence-v0.2.3/source-integrity.json", "utf8"),
  );
  for (const [index, asset] of plan.assets.entries()) {
    const original = evidence.find(
      (e: { file: string }) => e.file.split(".")[0] === asset.environmentId,
    );
    expect.soft(asset).not.toHaveProperty("verifiedLocalSource");
    expect.soft(asset).not.toHaveProperty("sourceDurationSeconds");
    expect.soft(asset.expectedSourceSha256).toBe(original.sha256);
    expect.soft(asset.sourceBytes).toBe(original.bytes);
    expect
      .soft(asset.directMediaDurationSeconds)
      .toBe(Number(original.probe.format.duration));
    expect.soft(asset.sourceVerification).toEqual({
      status: "verified",
      dateUtc: "2026-10-14",
      method:
        "SHA-256, byte count, ffprobe format.duration, full FFmpeg decode",
      evidence: ".agent/evidence-v0.2.3/source-integrity.json",
    });
    expect.soft(manifest.files[index]).toMatchObject(asset);
    expect
      .soft(manifest.files[index].sourceSha256)
      .toBe(asset.expectedSourceSha256);
  }
});

it("선택 구간은 승인 계획의 불변 provenance이며 manifest와 고지에 동일하게 남는다", () => {
  const plan = JSON.parse(
    readFileSync(".agent/AMBIENCE-SOURCES-v0.2.3.json", "utf8"),
  );
  const manifest = JSON.parse(
    readFileSync("public/ambience/manifest.json", "utf8"),
  );
  const notice = readFileSync("public/ambience/AMBIENCE_NOTICES.md", "utf8");
  const starts = {
    street: 35,
    mall: 36,
    theater: 45,
    department: 90,
    market: 24,
    subway: 65,
    cafe: 0,
    rain: 180,
  };
  expect(
    Object.fromEntries(
      plan.assets.map(
        (asset: {
          environmentId: string;
          selectedSegmentStartSeconds: number;
        }) => [asset.environmentId, asset.selectedSegmentStartSeconds],
      ),
    ),
  ).toEqual(starts);
  for (const asset of plan.assets) {
    const start = asset.selectedSegmentStartSeconds;
    expect(Number.isFinite(start)).toBe(true);
    expect(start).toBeGreaterThanOrEqual(0);
    expect(start + 12).toBeLessThanOrEqual(asset.directMediaDurationSeconds);
    const entry = manifest.files.find(
      (e: { environmentId: string }) => e.environmentId === asset.environmentId,
    );
    expect(entry.selectedSegmentStartSeconds).toBe(start);
    expect(entry.conversion.startSeconds).toBe(start);
    expect(entry.conversion.durationSeconds).toBe(12);
    const section = notice
      .split("## ")
      .find((s: string) =>
        s.startsWith(`${asset.displayName} — ${asset.environmentId}\n`),
      );
    expect(section).toContain(`source seconds ${start}–${start + 12}.`);
  }
});

it("v0.2.3 최종 ASR 증거는 정확한 WAV와 연결되며 청취·법적 인수로 주장하지 않는다", () => {
  const directory = ".agent/evidence-v0.2.3/privacy-screening";
  const screening = JSON.parse(
    readFileSync(`${directory}/final-asr.json`, "utf8"),
  );
  const manifest = JSON.parse(
    readFileSync(
      ".agent/evidence-v0.2.5/baseline/ambience/manifest.json",
      "utf8",
    ),
  );
  expect(screening.tool).toMatchObject({
    name: "faster-whisper",
    version: "1.2.1",
  });
  expect(screening.model).toMatchObject({ name: "tiny", multilingual: true });
  expect(screening.model.revision).toMatch(/^[a-f0-9]{40}$/);
  expect(screening.requiredEmptyPassed).toBe(true);
  expect(screening.requiredEmptyEnvironmentIds).toEqual([
    "mall",
    "market",
    "cafe",
  ]);
  expect(screening.method).toMatchObject({
    device: "cpu",
    computeType: "int8",
  });
  expect(screening.method.parameters).toMatchObject({
    task: "transcribe",
    language: null,
    beam_size: 5,
    best_of: 5,
    vad_filter: true,
    vad_parameters: null,
    condition_on_previous_text: true,
  });
  expect(screening.method.manifestSha256).toBe(
    createHash("sha256")
      .update(
        readFileSync(".agent/evidence-v0.2.5/baseline/ambience/manifest.json"),
      )
      .digest("hex"),
  );
  expect(screening.limitations).toMatchObject({
    screeningOnly: true,
    humanListeningAcceptance: false,
    legalPrivacyClearance: false,
    textIsVerifiedTranscript: false,
  });
  expect(
    screening.files.map((e: { environmentId: string }) => e.environmentId),
  ).toEqual(ENVIRONMENTS.map((e) => e.id));
  for (const entry of manifest.files) {
    const result = screening.files.find(
      (e: { environmentId: string }) => e.environmentId === entry.environmentId,
    );
    expect(result).toMatchObject({
      file: `public/ambience/${entry.file}`,
      sha256: entry.sha256,
      bytes: entry.bytes,
      durationSeconds: entry.duration,
      selectedSegmentStartSeconds: entry.selectedSegmentStartSeconds,
      sourceSha256: entry.sourceSha256,
    });
    expect(result.language).toEqual(expect.any(String));
    expect(result.languageProbability).toEqual(expect.any(Number));
    expect(Array.isArray(result.segments)).toBe(true);
    if (["mall", "market", "cafe"].includes(entry.environmentId)) {
      const starts: Record<string, number> = { mall: 36, market: 24, cafe: 0 };
      expect(result.selectedSegmentStartSeconds).toBe(
        starts[entry.environmentId],
      );
      expect(result.segments).toEqual([]);
      expect(result.text).toBe("");
      expect(result.info.duration_after_vad).toBe(0);
    }
  }
  const provenance = JSON.parse(
    readFileSync(`${directory}/earlier-asr-provenance.json`, "utf8"),
  );
  expect(provenance.textIsVerifiedTranscript).toBe(false);
  for (const record of provenance.records)
    expect(
      createHash("sha256")
        .update(readFileSync(`${directory}/${record.savedFile}`))
        .digest("hex"),
    ).toBe(record.sha256);
});

it("v0.2.3 역사 snapshot은 당시 101개 음성·다섯 현장음 보존을 증명한다", () => {
  const baseline = JSON.parse(
    readFileSync(
      ".agent/evidence-v0.2.3/privacy-screening/baseline.json",
      "utf8",
    ),
  );
  let voices = 0,
    unchangedAmbience = 0,
    changedAmbience = 0;
  for (const [path, before] of Object.entries(baseline.wavSha256)) {
    const actual = createHash("sha256")
      .update(readHistoricalBytes(path))
      .digest("hex");
    if (/public\/ambience\/(mall|market|cafe)\.wav$/.test(path)) {
      expect(actual).not.toBe(before);
      changedAmbience++;
    } else {
      expect(actual).toBe(before);
      if (path.startsWith("public/audio/")) voices++;
      else unchangedAmbience++;
    }
  }
  expect([voices, unchangedAmbience, changedAmbience]).toEqual([101, 5, 3]);
  for (const [path, before] of Object.entries(baseline.priorAgentFiles)) {
    if (baseline.mutableAgentFiles.includes(path)) continue;
    expect(
      createHash("sha256").update(readHistoricalBytes(path)).digest("hex"),
      path,
    ).toBe(before);
  }
});

it("v0.2.3 재시도는 market/cafe만 바꾸고 mall 및 실패·no-VAD 증거를 바이트 그대로 보존한다", () => {
  const directory = ".agent/evidence-v0.2.3/privacy-screening";
  const baseline = JSON.parse(
    readFileSync(`${directory}/retry-market24-cafe0/baseline.json`, "utf8"),
  );
  const hash = (path: string) =>
    createHash("sha256").update(readHistoricalBytes(path)).digest("hex");
  const prior = `${directory}/prior-attempt-market60-cafe180`;
  expect(hash(`${prior}/final-asr.json`)).toBe(
    "01c8a92fab9f8df7a5f778c5f7e478ce96bcedc33e11a1055bd29154a08b706b",
  );
  const failed = JSON.parse(readFileSync(`${prior}/final-asr.json`, "utf8"));
  expect(failed.requiredEmptyPassed).toBe(false);
  expect(failed.limitations).toMatchObject({
    humanListeningAcceptance: false,
    legalPrivacyClearance: false,
  });
  for (const [environmentId, start, count] of [
    ["mall", 36, 0],
    ["market", 60, 3],
    ["cafe", 180, 1],
  ]) {
    const result = failed.files.find(
      (f: { environmentId: string }) => f.environmentId === environmentId,
    );
    expect(result.selectedSegmentStartSeconds).toBe(start);
    expect(result.segments).toHaveLength(count as number);
  }
  expect(hash(`${directory}/no-vad-asr.json`)).toBe(
    "14b42ee0b80c194caa3c0de2644f647d6d7720c60c2208374778619f47c2e969",
  );
  for (const [path, before] of Object.entries(
    baseline.immutablePriorAttemptFiles,
  ))
    expect(hash(path), path).toBe(before);
  for (const [path, before] of Object.entries(baseline.priorAgentFiles)) {
    if (!baseline.mutableAgentFiles.includes(path))
      expect(hash(path), path).toBe(before);
  }
  const changed: string[] = [];
  let voices = 0,
    preservedAmbience = 0;
  for (const [path, before] of Object.entries(baseline.wavSha256)) {
    if (/public\/ambience\/(market|cafe)\.wav$/.test(path)) {
      expect(hash(path), path).not.toBe(before);
      changed.push(path);
    } else {
      expect(hash(path), path).toBe(before);
      if (path.startsWith("public/audio/")) voices++;
      else preservedAmbience++;
    }
  }
  expect(changed.sort()).toEqual([
    "public/ambience/cafe.wav",
    "public/ambience/market.wav",
  ]);
  expect([voices, preservedAmbience]).toEqual([101, 6]);
});

// 이번 릴리스는 8 배경을 모두 바꾸되 원본·선택 구간·권리와 101 voice/660 역사 파일을 보존한다.
it("v0.2.5는 이전 증거·음성 바이트와 모든 출처 필드를 그대로 보존한다", () => {
  const baseline = JSON.parse(
    readFileSync(".agent/evidence-v0.2.5/baseline.json", "utf8"),
  );
  for (const [path, expected] of Object.entries({
    ...baseline.immutable,
    ...baseline.voiceWavs,
  }))
    expect(
      createHash("sha256").update(readFileSync(path)).digest("hex"),
      path,
    ).toBe(expected);
  const prior = JSON.parse(
    readFileSync(
      ".agent/evidence-v0.2.5/baseline/ambience/manifest.json",
      "utf8",
    ),
  );
  const current = JSON.parse(
    readFileSync("public/ambience/manifest.json", "utf8"),
  );
  for (const [index, before] of prior.files.entries()) {
    const after = current.files[index];
    for (const key of [
      "environmentId",
      "pageUrl",
      "directUrl",
      "creator",
      "license",
      "licenseUrl",
      "attribution",
      "expectedSourceSha256",
      "sourceBytes",
      "sourceSha256",
      "directMediaDurationSeconds",
      "sourceVerification",
      "selectedSegmentStartSeconds",
      "outputLicense",
    ])
      expect(after[key], key).toEqual(before[key]);
    expect(after.sha256).not.toBe(before.sha256);
  }
});

// 새 바이트의 ASR은 과거 승인으로 대체하지 않는다. 성공을 강제하려고 결과를 지우면 실패한다.
it("거부된 loudnorm WAV의 ASR 실패와 privacy 인수 경계를 보존한다", () => {
  const screening = JSON.parse(
    readFileSync(".agent/evidence-v0.2.5/final-asr.json", "utf8"),
  );
  const manifestBytes = readFileSync(
    ".agent/evidence-v0.2.5/review-fixes/rejected-loudnorm-bundle/manifest.json",
  );
  const manifest = JSON.parse(manifestBytes.toString());
  expect(screening.method.manifestSha256).toBe(
    createHash("sha256").update(manifestBytes).digest("hex"),
  );
  expect(screening.requiredEmptyPassed).toBe(false);
  expect(screening.limitations).toMatchObject({
    humanListeningAcceptance: false,
    legalPrivacyClearance: false,
    textIsVerifiedTranscript: false,
  });
  for (const entry of manifest.files) {
    const record = screening.files.find(
      (r: { environmentId: string }) => r.environmentId === entry.environmentId,
    );
    expect(record).toMatchObject({
      sha256: entry.sha256,
      sourceSha256: entry.sourceSha256,
      selectedSegmentStartSeconds: entry.selectedSegmentStartSeconds,
    });
    expect(record.segments).toHaveLength(
      entry.environmentId === "market" ? 1 : 0,
    );
  }
});

// 수정본은 정확한 새 WAV로 같은 VAD gate를 통과해야 하며 과거 결과를 승계하지 않는다.
it("v0.2.5 최종 WAV의 동일 ASR VAD gate는 requiredEmptyPassed=true여야 한다", () => {
  const screening = JSON.parse(
    readFileSync(".agent/evidence-v0.2.5/review-fixes/final-asr.json", "utf8"),
  );
  const prior = JSON.parse(
    readFileSync(".agent/evidence-v0.2.5/final-asr.json", "utf8"),
  );
  const bytes = readFileSync("public/ambience/manifest.json");
  const manifest = JSON.parse(bytes.toString());
  expect(screening.tool).toEqual(prior.tool);
  expect(screening.model).toEqual(prior.model);
  for (const key of [
    "device",
    "computeType",
    "cpuThreads",
    "numWorkers",
    "randomSeed",
    "parameters",
    "vadModelSha256",
  ])
    expect(screening.method[key]).toEqual(prior.method[key]);
  expect(screening.method.manifestSha256).toBe(
    createHash("sha256").update(bytes).digest("hex"),
  );
  expect(screening.requiredEmptyEnvironmentIds).toEqual([
    "mall",
    "market",
    "cafe",
  ]);
  expect(screening.requiredEmptyPassed).toBe(true);
  expect(screening.limitations).toMatchObject({
    humanListeningAcceptance: false,
    legalPrivacyClearance: false,
    textIsVerifiedTranscript: false,
  });
  for (const entry of manifest.files) {
    const row = screening.files.find(
      (r: { environmentId: string }) => r.environmentId === entry.environmentId,
    );
    expect(row).toMatchObject({
      sha256: entry.sha256,
      bytes: entry.bytes,
      sourceSha256: entry.sourceSha256,
      selectedSegmentStartSeconds: entry.selectedSegmentStartSeconds,
    });
    if (["mall", "market", "cafe"].includes(entry.environmentId)) {
      expect(row.segments).toEqual([]);
      expect(row.text).toBe("");
      expect(row.info.duration_after_vad).toBe(0);
    }
  }
});
