import { afterEach, describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type {
  ActivityReportSection,
  ObservabilityReport,
  ObservabilityReportQuery,
  ReportBucket,
  ReportCoverageStatus,
} from "@overdeck/report-contract";
import { OBSERVABILITY_REPORT_SCHEMA_VERSION } from "@overdeck/report-contract";
import { ReportHistoryStore } from "./history-store";
import {
  backfillReportHistory,
  buildHistoryReportSection,
  reportHistoryBuckets,
} from "./history-report";

const roots: string[] = [];
const QUERY: ObservabilityReportQuery = {
  from: "2026-08-16T10:00:00.000Z",
  to: "2026-08-16T12:00:00.000Z",
  timezone: "UTC",
};

function store(): ReportHistoryStore {
  const parent = join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "overdeck", "tests", "report-history");
  mkdirSync(parent, { recursive: true });
  const root = mkdtempSync(join(parent, "projection-"));
  roots.push(root);
  return new ReportHistoryStore(join(root, "history.sqlite"));
}

function dimensionKey(query: ObservabilityReportQuery): string {
  return JSON.stringify({
    projects: [...(query.projects ?? [])].sort(),
    sources: [...(query.sources ?? [])].sort(),
    severity: query.severity ?? null,
    q: query.q ?? null,
  });
}

function bucket(
  bucketStart: string,
  value: number,
  overrides: Partial<ReportBucket> = {},
): ReportBucket {
  const start = Date.parse(bucketStart);
  const bucketEnd = new Date(start + 60 * 60 * 1_000).toISOString();
  return {
    bucketStart,
    bucketEnd,
    metricKey: "activity.recorded-events",
    metricVersion: 1,
    label: "Recorded events",
    unit: "count",
    dimensionKey: dimensionKey(QUERY),
    value,
    coverage: { status: "complete", key: "complete", gaps: [] },
    sourceWatermark: { actions: bucketEnd },
    generatedAt: bucketEnd,
    backfilled: true,
    ...overrides,
  };
}

function report(
  query: ObservabilityReportQuery,
  recordedEvents = 4,
  failures = 1,
  status: ReportCoverageStatus = "complete",
): ObservabilityReport {
  const activity: ActivityReportSection = {
    kind: "activity",
    status,
    title: "Activity and source trust",
    metrics: [
      { id: "recorded-events", label: "Recorded events", value: recordedEvents, unit: "count", coverage: status },
      { id: "failures", label: "Failures", value: failures, unit: "count", coverage: status },
      { id: "active-sources", label: "Active sources", value: 1, unit: "count", coverage: status },
      { id: "source-problems", label: "Source problems", value: 0, unit: "count", coverage: status },
    ],
    series: [],
    categoryBreakdown: [],
    attention: [],
    sources: [],
    truncated: false,
    errors: [],
  };
  return {
    schemaVersion: OBSERVABILITY_REPORT_SCHEMA_VERSION,
    query,
    generatedAt: query.to,
    coverage: {
      status,
      from: query.from,
      to: query.to,
      generatedAt: query.to,
      sources: [{
        id: "actions",
        label: "Actions",
        category: "service",
        authority: "authoritative",
        status: "ok",
        storage: "Collector state",
        queryBounds: "Requested range",
        coverageFrom: query.from,
        earliest: query.from,
        latest: query.to,
        matchingRecords: recordedEvents,
        retainedRecords: recordedEvents,
        skipped: 0,
        href: "/logs/actions",
      }],
      gaps: [],
    },
    sections: [activity],
    filters: { projects: query.projects ?? [], sources: [{ id: "actions", label: "Actions" }] },
  };
}

afterEach(() => {
  while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true });
});

describe("report history projection", () => {
  test("turns authoritative activity metrics into deterministic filter-specific buckets", () => {
    const buckets = reportHistoryBuckets(report({ ...QUERY, projects: ["zeta", "alpha"] }), QUERY.from, QUERY.to, true);
    expect(buckets.map((row) => [row.metricKey, row.value])).toEqual([
      ["activity.recorded-events", 4],
      ["activity.failures", 1],
    ]);
    expect(buckets[0]).toMatchObject({
      dimensionKey: JSON.stringify({ projects: ["alpha", "zeta"], sources: [], severity: null, q: null }),
      generatedAt: QUERY.to,
      backfilled: true,
    });
  });

  test("backfills newest missing hours first with bounded request work", () => {
    const history = store();
    const requested: string[] = [];
    const outcome = backfillReportHistory({
      store: history,
      nowMs: Date.parse("2026-08-16T12:30:00.000Z"),
      baseQuery: { timezone: "UTC" },
      maxHours: 48,
      maxBuilds: 2,
      buildReport: (query) => {
        requested.push(query.from);
        return report(query);
      },
    });
    expect(outcome).toEqual({ inserted: 4, unchanged: 0, attemptedHours: 2 });
    expect(requested).toEqual(["2026-08-16T11:00:00.000Z", "2026-08-16T10:00:00.000Z"]);
    history.close();
  });

  test("reports compatible current and preceding totals without inventing missing values", () => {
    const history = store();
    for (const [start, value] of [
      ["2026-08-16T08:00:00.000Z", 1],
      ["2026-08-16T09:00:00.000Z", 2],
      ["2026-08-16T10:00:00.000Z", 3],
      ["2026-08-16T11:00:00.000Z", 4],
    ] as const) history.writeReportBucket(bucket(start, value));

    const section = buildHistoryReportSection(QUERY, history);
    expect(section.metrics.find((metric) => metric.key === "activity.recorded-events")).toMatchObject({
      current: 7,
      previous: 3,
      delta: 4,
      direction: "up",
      coverage: "complete",
      comparisonCompatible: true,
    });
    history.close();
  });

  test("keeps missing hours as explicit gaps rather than charting a zero or bridging the interval", () => {
    const history = store();
    history.writeReportBucket(bucket("2026-08-16T10:00:00.000Z", 3));
    const section = buildHistoryReportSection(QUERY, history);
    const series = section.series.find((candidate) => candidate.key === "activity.recorded-events");
    expect(series?.points).toEqual([{ ts: "2026-08-16T10:00:00.000Z", value: 3 }]);
    expect(series?.gaps).toEqual([{
      from: "2026-08-16T11:00:00.000Z",
      to: "2026-08-16T12:00:00.000Z",
      reason: "No compatible retained history bucket was recorded for this interval.",
    }]);
    expect(section.metrics.find((metric) => metric.key === "activity.recorded-events")).toMatchObject({
      current: 3,
      previous: null,
      delta: null,
      coverage: "partial",
      comparisonCompatible: false,
    });
    history.close();
  });

  test("refuses comparisons across metric versions", () => {
    const history = store();
    history.writeReportBucket(bucket("2026-08-16T08:00:00.000Z", 1));
    history.writeReportBucket(bucket("2026-08-16T09:00:00.000Z", 2));
    history.writeReportBucket(bucket("2026-08-16T10:00:00.000Z", 3, { metricVersion: 2 }));
    history.writeReportBucket(bucket("2026-08-16T11:00:00.000Z", 4, { metricVersion: 2 }));
    const metric = buildHistoryReportSection(QUERY, history).metrics
      .find((candidate) => candidate.key === "activity.recorded-events");
    expect(metric).toMatchObject({ current: 7, previous: null, delta: null, comparisonCompatible: false });
    expect(metric?.comparisonReason).toContain("same metric version");
    history.close();
  });

  test("reads only the selected filter dimension", () => {
    const history = store();
    const projectQuery = { ...QUERY, projects: ["overdeck"] };
    for (const start of ["2026-08-16T10:00:00.000Z", "2026-08-16T11:00:00.000Z"] as const) {
      history.writeReportBucket(bucket(start, 100));
      history.writeReportBucket(bucket(start, 2, { dimensionKey: dimensionKey(projectQuery) }));
    }
    const metric = buildHistoryReportSection(projectQuery, history).metrics
      .find((candidate) => candidate.key === "activity.recorded-events");
    expect(metric?.current).toBe(4);
    history.close();
  });
});
