import { describe, expect, test } from "bun:test";
import type { ActivityEvent, ActivityReadResponse, ActivitySourceCoverage } from "../activity/types";
import type { ActivityReportSection, ObservabilityReport, ObservabilityReportQuery } from "@overdeck/report-contract";
import { buildActivityObservabilityReport } from "./activity-report";

const QUERY: ObservabilityReportQuery = {
  from: "2026-08-15T12:00:00.000Z",
  to: "2026-08-16T12:00:00.000Z",
  timezone: "UTC",
};

function activitySection(report: ObservabilityReport): ActivityReportSection {
  const section = report.sections[0];
  if (section?.kind !== "activity") {
    throw new Error(`expected an activity report section, got ${section?.kind ?? "none"}`);
  }
  return section;
}

function coverage(overrides: Partial<ActivitySourceCoverage> = {}): ActivitySourceCoverage {
  return {
    id: "actions",
    label: "collector actions",
    category: "service",
    path: "/state/actions.jsonl",
    authority: "authoritative",
    storage: "Local source file",
    retention: "Source-managed",
    queryBounds: "Requested range",
    status: "ok",
    records: 2,
    totalRecords: 4,
    coverageFrom: QUERY.from,
    earliest: QUERY.from,
    latest: "2026-08-16T11:00:00.000Z",
    skipped: 0,
    ...overrides,
  };
}

function event(overrides: Partial<ActivityEvent> = {}): ActivityEvent {
  return {
    id: "actions:1",
    ts: "2026-08-16T11:00:00.000Z",
    category: "service",
    source: "actions",
    actor: "human",
    severity: "error",
    title: "deploy failed",
    result: "failure",
    project: "overdeck",
    ...overrides,
  };
}

function response(overrides: Partial<ActivityReadResponse> = {}): ActivityReadResponse {
  return {
    events: [event()],
    total: 2,
    truncated: false,
    limit: 2000,
    categoryCounts: [{ category: "service", count: 2 }],
    projectCounts: [{ project: "overdeck", count: 2 }],
    sourceCounts: [{ source: "actions", count: 2 }],
    errorCount: 1,
    series: [{ id: "logVolume", label: "Log volume", status: "available", coverageFrom: QUERY.from, points: [{ ts: QUERY.from, value: 2 }] }],
    coverage: [coverage()],
    suppressedNotifications: { status: "ok", path: "/state/suppressed.json", rows: [], sourceCount: 0, attemptCount: 0, skipped: 0 },
    ...overrides,
  };
}

describe("buildActivityObservabilityReport", () => {
  test("builds complete metrics, source trust, trend, and event evidence without client inference", () => {
    let received: unknown;
    const report = buildActivityObservabilityReport(QUERY, {
      now: () => Date.parse("2026-08-16T12:00:05.000Z"),
      readActivity: (options) => { received = options; return response(); },
    });

    expect(received).toMatchObject({ from: QUERY.from, to: QUERY.to, limit: 2000 });
    expect(report.coverage.status).toBe("complete");
    expect(activitySection(report).metrics.map((metric) => [metric.id, metric.value])).toEqual([
      ["recorded-events", 2],
      ["failures", 1],
      ["active-sources", 1],
      ["source-problems", 0],
    ]);
    expect(activitySection(report).series[0]?.points).toEqual([{ ts: QUERY.from, value: 2 }]);
    expect(activitySection(report).sources[0]).toMatchObject({ matchingRecords: 2, retainedRecords: 4, href: expect.stringContaining("/logs/actions?") });
    expect(activitySection(report).attention[0]).toMatchObject({ kind: "event", evidenceQuery: { sourceId: "actions", recordId: "actions:1" } });
  });

  test("marks source errors, skipped data, and short retention as named partial coverage", () => {
    const report = buildActivityObservabilityReport(QUERY, {
      now: () => Date.parse(QUERY.to),
      readActivity: () => response({
        coverage: [
          coverage({ id: "actions", skipped: 2, coverageFrom: "2026-08-16T00:00:00.000Z" }),
          coverage({ id: "controller", label: "controller", status: "error", records: 0, totalRecords: 0, coverageFrom: undefined, earliest: undefined, latest: undefined, error: "permission denied: /private/state/controller.jsonl" }),
        ],
        sourceCounts: [{ source: "actions", count: 2 }],
        series: [{ id: "logVolume", label: "Log volume", status: "unavailable", reason: "Coverage incomplete", points: [] }],
      }),
    });

    expect(report.coverage.status).toBe("partial");
    expect(report.coverage.gaps.map((gap) => gap.reason)).toEqual(expect.arrayContaining([
      expect.stringContaining("skipped 2"),
      expect.stringContaining("starts after"),
      "controller could not be read.",
    ]));
    expect(JSON.stringify(report)).not.toContain("/private/state/controller.jsonl");
    expect(JSON.stringify(report)).not.toContain("permission denied");
    expect(activitySection(report).attention.map((row) => row.kind)).toEqual(expect.arrayContaining(["source-error", "skipped-records", "event"]));
    expect(activitySection(report).metrics.find((metric) => metric.id === "source-problems")?.value).toBe(2);
    expect(activitySection(report).series[0]?.coverage).toBe("unavailable");
  });

  test("returns unavailable values instead of zeros when no selected source is readable", () => {
    const report = buildActivityObservabilityReport({ ...QUERY, sources: ["actions"] }, {
      now: () => Date.parse(QUERY.to),
      readActivity: (options) => {
        expect(options.sourceIds).toEqual(["actions"]);
        return response({
          events: [],
          total: 0,
          errorCount: 0,
          categoryCounts: [],
          sourceCounts: [],
          coverage: [coverage({ status: "absent", records: 0, totalRecords: 0, coverageFrom: undefined, earliest: undefined, latest: undefined })],
          series: [{ id: "logVolume", label: "Log volume", status: "unavailable", reason: "No healthy source", points: [] }],
        });
      },
    });

    expect(report.coverage.status).toBe("unavailable");
    expect(activitySection(report).metrics.find((metric) => metric.id === "recorded-events")?.value).toBeNull();
    expect(activitySection(report).metrics.find((metric) => metric.id === "failures")?.value).toBeNull();
  });

  test("labels an unproven retained window instead of treating an empty readable source as complete", () => {
    const report = buildActivityObservabilityReport(QUERY, {
      now: () => Date.parse(QUERY.to),
      readActivity: () => response({
        events: [], total: 0, errorCount: 0, sourceCounts: [], categoryCounts: [],
        coverage: [coverage({ records: 0, totalRecords: 0, coverageFrom: undefined, earliest: undefined, latest: undefined })],
        series: [{ id: "logVolume", label: "Log volume", status: "unavailable", reason: "No retained window", points: [] }],
      }),
    });

    expect(report.coverage.status).toBe("partial");
    expect(report.coverage.gaps[0]?.reason).toContain("full-window coverage cannot be proven");
  });
});
