/**
 * Example Benchmark provisioning tests:
 * default_agent initialization pre-seeds benchmarks/example-benchmark/ (a parseable config,
 * runs=2, a scoreboard with three self-consistent evaluations); an ordinary Agent gets none;
 * provisioning is skipped idempotently when benchmarks/ already exists.
 */
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { parse as parseToml } from "smol-toml";
import { parse as parseYaml } from "yaml";
import {
  DEFAULT_AGENT_ID,
  DEFAULT_PROJECT_ID,
  EXAMPLE_BENCHMARK_ID,
  benchmarksDir,
  buildExampleScoreboard,
  loadOrInitAgentState,
  provisionProjectAgents,
} from "../src/state/index.js";

let tmpRoot: string;
let prevHome: string | undefined;

beforeEach(async () => {
  prevHome = process.env.PENGUIN_HOME;
  tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "penguin-bench-"));
  process.env.PENGUIN_HOME = tmpRoot;
});

afterEach(async () => {
  if (prevHome === undefined) delete process.env.PENGUIN_HOME;
  else process.env.PENGUIN_HOME = prevHome;
  await fs.rm(tmpRoot, { recursive: true, force: true });
});

async function exists(p: string): Promise<boolean> {
  try {
    await fs.access(p);
    return true;
  } catch {
    return false;
  }
}

interface RunScore {
  score: number;
  cost: number | null;
  duration_ms: number;
  session_id: string;
}
interface CaseScore extends Omit<RunScore, "session_id"> {
  case: string;
  runs: RunScore[];
}
interface Evaluation extends Omit<CaseScore, "case" | "runs"> {
  time: string;
  version: number;
  provider: string;
  model_id: string;
  thinking_level: string;
  summary_title: string;
  summary: string;
  cases: CaseScore[];
}

describe("example benchmark provisioning", () => {
  it("default_agent init creates a parseable example benchmark (config + scoreboard + cases)", async () => {
    await loadOrInitAgentState();
    const dir = path.join(
      benchmarksDir(tmpRoot, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID),
      EXAMPLE_BENCHMARK_ID,
    );

    // benchmark_config.toml: title/description/runs=2; contains no model reference (the model
    // is recorded on each evaluation instead).
    const config = parseToml(await fs.readFile(path.join(dir, "benchmark_config.toml"), "utf8"));
    expect(config.title).toBe("Example Benchmark");
    expect(String(config.description)).toContain("built-in example");
    expect(String(config.description)).toContain("Replace it with your own");
    expect(Number(config.runs)).toBe(2);
    expect(config).not.toHaveProperty("provider");
    expect(config).not.toHaveProperty("model_id");

    // Two cases: statement/ and rubric/ each use README.md as the index.
    for (const caseId of ["CASE-001-file-summary", "CASE-002-data-cleanup"]) {
      const statement = await fs.readFile(path.join(dir, caseId, "statement", "README.md"), "utf8");
      const rubric = await fs.readFile(path.join(dir, caseId, "rubric", "README.md"), "utf8");
      expect(statement.length).toBeGreaterThan(50);
      expect(rubric).toContain("pts");
      expect(rubric).toContain("max 100 points");
    }

    // scoreboard.yaml: 3 evaluations, with version/time increasing and scores rising, and
    // 2 runs per case.
    const scoreboard = parseYaml(await fs.readFile(path.join(dir, "scoreboard.yaml"), "utf8")) as {
      evaluations: Evaluation[];
    };
    expect(scoreboard.evaluations).toHaveLength(3);
    expect(JSON.stringify(scoreboard)).not.toContain("max_score");
    expect(scoreboard.evaluations.map((e) => e.version)).toEqual([1, 2, 3]);
    const times = scoreboard.evaluations.map((e) => new Date(e.time).getTime());
    expect(times[0]!).toBeLessThan(times[1]!);
    expect(times[1]!).toBeLessThan(times[2]!);
    const scores = scoreboard.evaluations.map((e) => e.score);
    expect(scores[0]!).toBeLessThan(scores[1]!);
    expect(scores[1]!).toBeLessThan(scores[2]!);
    // Each evaluation carries the actual model used for that run (as a pair) and a summary
    // title; the example consistently uses deepseek-v4-pro.
    expect(scoreboard.evaluations.map((e) => e.model_id)).toEqual([
      "deepseek-v4-pro",
      "deepseek-v4-pro",
      "deepseek-v4-pro",
    ]);
    for (const e of scoreboard.evaluations) {
      expect(e.provider).toBe("deepseek");
      expect(e.thinking_level).toBe("medium");
      expect(e.summary_title.length).toBeGreaterThan(0);
      expect(e.summary.toLowerCase()).toContain("example");
      expect(e.cases).toHaveLength(2);
      for (const c of e.cases) {
        expect(c.runs).toHaveLength(2);
        for (const r of c.runs) {
          expect(r.session_id).toMatch(/^session-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-[0-9a-f]{8}$/);
        }
      }
    }
  });

  it("scoreboard numbers preserve Run cost precision and use model-written averages", async () => {
    const { evaluations } = buildExampleScoreboard();
    expect(evaluations[0]?.cases[0]?.runs.map((run) => run.cost)).toEqual([0.012, 0.014]);
    const avg = (vals: number[]): number => vals.reduce((a, b) => a + b, 0) / vals.length;
    const knownCostAvg = (vals: Array<number | null>): number | null => {
      const known = vals.filter((value): value is number => value !== null);
      return known.length > 0 ? avg(known) : null;
    };
    for (const e of evaluations) {
      for (const c of e.cases) {
        expect(c.runs.every((run) => run.score >= 0 && run.score <= 100)).toBe(true);
        expect(c.score).toBe(Math.round(avg(c.runs.map((r) => r.score)) * 100) / 100);
        const expectedCost = knownCostAvg(c.runs.map((r) => r.cost));
        expect(c.cost).toBe(
          expectedCost === null ? null : Math.round(expectedCost * 1_000_000) / 1_000_000,
        );
        expect(c.duration_ms).toBe(Math.round(avg(c.runs.map((r) => r.duration_ms))));
      }
      expect(e.score).toBe(Math.round(avg(e.cases.map((c) => c.score)) * 100) / 100);
      const expectedCost = knownCostAvg(e.cases.map((c) => c.cost));
      expect(e.cost).toBe(
        expectedCost === null ? null : Math.round(expectedCost * 1_000_000) / 1_000_000,
      );
      expect(e.duration_ms).toBe(Math.round(avg(e.cases.map((c) => c.duration_ms))));
    }
  });

  it("provisionProjectAgents also seeds the example benchmark for default_agent", async () => {
    await provisionProjectAgents({ root: tmpRoot, projectId: "proj_x" });
    expect(
      await exists(
        path.join(benchmarksDir(tmpRoot, "proj_x", DEFAULT_AGENT_ID), EXAMPLE_BENCHMARK_ID),
      ),
    ).toBe(true);
  });

  it("does not create benchmarks for a non-default agent", async () => {
    await loadOrInitAgentState({ agentId: "worker" });
    expect(await exists(benchmarksDir(tmpRoot, DEFAULT_PROJECT_ID, "worker"))).toBe(false);
  });

  it("skips provisioning when benchmarks/ already exists (idempotent, no clobber)", async () => {
    const dir = benchmarksDir(tmpRoot, DEFAULT_PROJECT_ID, DEFAULT_AGENT_ID);
    await fs.mkdir(dir, { recursive: true });
    await loadOrInitAgentState();
    expect(await fs.readdir(dir)).toEqual([]);
  });
});
