/**
 * `test_case_facts`, one row per run × task × model × variant (D0091).
 *
 * The workhorse table: per-test-case cost, tokens, latency, pass/fail, the
 * composite and per-dimension scores, and the output-quality flags, joined to
 * the feature area. Answers "cost/tokens/latency and score per feature area
 * over time", none of which is queryable today.
 *
 * ## Two sources, joined per test case
 *
 * The grain is one `rawResults` entry (per run × mode × task × model, always
 * present when the eval ran, and its `sourceUri` is the natural `gs_uri`). The
 * feature `area`, the normalized composite / per-dimension scores, and the
 * output-quality flags come from the enriched `scoreSummary.testResults[]`
 * (`StoredTestResult`), looked up by (task, model, variant). `passed`, cost,
 * tokens, and latency come from the raw entry itself. A run missing either
 * artifact yields zero rows (the engine only runs a descriptor when every
 * `sourceArtifacts` type is present).
 *
 * Derives from the canonical `ScoreSummary` / `StoredTestResult` core types
 * and the `splitTaskVariant` axis convention (D0045, derive, don't redeclare);
 * the promptfoo raw shape has no canonical core type, so its read-projection is
 * a local minimal interface, narrowed field-by-field.
 *
 * @see packages/core/src/types/index.ts, ScoreSummary, StoredTestResult
 * @see docs/design-docs/bigquery-analytics-fact-loader.md
 */
import { z } from "zod";
import { splitTaskVariant } from "../../artifact-capture/association.js";
import { defineFactTable, } from "../descriptor.js";
import { axisString, boolOrNull, LOADED_AT_COLUMN, nonNegIntOrNull, numOrNull, runFactBase, runFactBaseShape, RUN_FACT_BASE_COLUMNS, } from "./common.js";
const rowSchema = z.object({
    ...runFactBaseShape,
    mode: z.string(),
    variant: z.string().nullable(),
    task: z.string(),
    model_id: z.string(),
    area: z.string().nullable(),
    passed: z.boolean().nullable(),
    composite_score: z.number().nullable(),
    cost: z.number().nullable(),
    prompt_tokens: z.number().nullable(),
    completion_tokens: z.number().nullable(),
    cached_tokens: z.number().nullable(),
    total_tokens: z.number().nullable(),
    latency_ms: z.number().nullable(),
    output_failure: z.boolean().nullable(),
    output_truncated: z.boolean().nullable(),
    dimension_scores: z.record(z.string(), z.number()).nullable(),
});
const schema = [
    ...RUN_FACT_BASE_COLUMNS,
    { name: "mode", type: "STRING", description: "Variant-or-mode axis value." },
    {
        name: "variant",
        type: "STRING",
        description: "Literacy variant, or null.",
    },
    { name: "task", type: "STRING", description: "Task id (variant stripped)." },
    { name: "model_id", type: "STRING", description: "Model under evaluation." },
    { name: "area", type: "STRING", description: "Resolved feature area." },
    { name: "passed", type: "BOOL", description: "Grading result pass/fail." },
    {
        name: "composite_score",
        type: "FLOAT64",
        description: "Weighted composite (0–100).",
    },
    { name: "cost", type: "FLOAT64", description: "Per-test cost (USD)." },
    { name: "prompt_tokens", type: "INT64" },
    { name: "completion_tokens", type: "INT64" },
    { name: "cached_tokens", type: "INT64" },
    { name: "total_tokens", type: "INT64" },
    { name: "latency_ms", type: "INT64", description: "Response latency (ms)." },
    { name: "output_failure", type: "BOOL", description: "No gradeable output." },
    { name: "output_truncated", type: "BOOL", description: "Output truncated." },
    {
        name: "dimension_scores",
        type: "JSON",
        description: "Per-dimension scores.",
    },
    LOADED_AT_COLUMN,
];
/** Derive the literacy variant carried in the mode axis, if any. */
function variantOf(mode) {
    return mode === "gold" || mode === "baseline" ? mode : null;
}
/** Join key for aligning a raw entry to its `StoredTestResult`. */
function joinKey(task, model, variant) {
    return JSON.stringify([task, model, variant]);
}
/** Build the (task, model, variant) → StoredTestResult lookup from the summary. */
function testResultIndex(summary) {
    const index = new Map();
    const results = summary?.testResults;
    if (!Array.isArray(results))
        return index;
    for (const tr of results) {
        if (tr == null || typeof tr !== "object")
            continue;
        const { task } = splitTaskVariant(tr.taskId ?? "");
        index.set(joinKey(task, tr.modelId ?? "", tr.variant ?? null), tr);
    }
    return index;
}
/** Flatten a StoredTestResult's dimensions to a `{ dimension: score }` map. */
function dimensionScores(tr) {
    if (!tr || !Array.isArray(tr.dimensions) || tr.dimensions.length === 0) {
        return null;
    }
    const out = {};
    for (const d of tr.dimensions) {
        if (d && typeof d.dimension === "string" && typeof d.score === "number") {
            out[d.dimension] = d.score;
        }
    }
    return Object.keys(out).length > 0 ? out : null;
}
// ---------------------------------------------------------------------------
// Extractor
// ---------------------------------------------------------------------------
function extract(ctx) {
    const summaryEntry = ctx.artifacts.entries("scoreSummary")[0];
    const byTestCase = testResultIndex(summaryEntry?.payload);
    return ctx.artifacts
        .entries("rawResults")
        .map((entry) => {
        const mode = axisString(entry.association.mode);
        const task = axisString(entry.association.task);
        const modelId = axisString(entry.association.model);
        const variant = variantOf(mode);
        const tr = byTestCase.get(joinKey(task, modelId, variant));
        const raw = entry.payload;
        const tokenUsage = raw.response?.tokenUsage;
        const trTokens = tr?.tokenUsage;
        return {
            ...runFactBase(ctx, entry.sourceUri),
            mode,
            variant,
            task,
            model_id: modelId,
            area: tr?.area ?? null,
            passed: boolOrNull(raw.gradingResult?.pass),
            // Composite is the normalized 0–100 AILF score (gold-variant only).
            // Not falling back to promptfoo's top-level `score`, which is a 0–1
            // assertion aggregate, mixing the two scales in one column is wrong.
            composite_score: tr?.compositeScore ?? null,
            cost: numOrNull(raw.cost) ?? tr?.cost ?? null,
            prompt_tokens: nonNegIntOrNull(tokenUsage?.prompt) ??
                nonNegIntOrNull(trTokens?.prompt),
            completion_tokens: nonNegIntOrNull(tokenUsage?.completion) ??
                nonNegIntOrNull(trTokens?.completion),
            cached_tokens: nonNegIntOrNull(tokenUsage?.cached) ??
                nonNegIntOrNull(trTokens?.cached),
            total_tokens: nonNegIntOrNull(tokenUsage?.total) ??
                nonNegIntOrNull(trTokens?.total),
            latency_ms: nonNegIntOrNull(raw.latencyMs) ?? tr?.latencyMs ?? null,
            output_failure: tr?.outputFailure ?? null,
            output_truncated: tr?.responseOutputTruncated ?? null,
            dimension_scores: dimensionScores(tr),
        };
    });
}
export const testCaseFacts = defineFactTable({
    table: "test_case_facts",
    dataset: "ailf_facts",
    sourceArtifacts: ["rawResults", "scoreSummary"],
    schema,
    rowSchema,
    partitionField: "completed_at",
    clusterFields: ["mode", "area", "model_id"],
    mergeKey: ["run_id", "mode", "task", "model_id"],
    extract,
});
export { rowSchema as testCaseFactRowSchema };
