// @effect-diagnostics nodeBuiltinImport:off - evidence driver seeds synthetic DBs on disk.
import * as NodeFSP from "node:fs/promises";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as NodeSqlite from "node:sqlite";

import { assert, describe, it } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as HostProcess from "@t3tools/shared/HostProcess";
import {
  ProviderDriverKind,
  ProviderInstanceId,
  UsageDay,
  type UsageSummaryInput,
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { HttpClient, HttpClientResponse } from "effect/http";

import * as ServerConfig from "../config.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as Sqlite from "../persistence/Sqlite.ts";
import * as CodexUsageHistory from "./CodexUsageHistory.ts";
import * as CursorAccountReader from "@t3tools/provider-cursor/server/CursorAccountReader";
import * as CursorKeychain from "@t3tools/provider-cursor/server/CursorKeychain";
import * as CursorUsageAccounts from "@t3tools/provider-cursor/server/CursorUsageAccounts";
import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
import * as AntigravityUsage from "../provider/Drivers/AntigravityUsage.ts";
import * as ProviderHostLive from "../provider/ProviderHostLive.ts";
import { readAntigravityUsage } from "../provider/Drivers/antigravityUsageReader.ts";
import * as UsageService from "./UsageService.ts";

function protoNumber(field: number, value: number): number[] {
  const varint = (number: number) => {
    const bytes: number[] = [];
    do {
      const byte = number % 128;
      number = Math.floor(number / 128);
      bytes.push(byte + (number > 0 ? 128 : 0));
    } while (number > 0);
    return bytes;
  };
  return [...varint(field * 8), ...varint(value)];
}
function protoBytes(field: number, bytes: readonly number[]): number[] {
  const encoded = protoNumber(field, bytes.length);
  encoded[0] = encoded[0]! + 2;
  return [...encoded, ...bytes];
}
function protoText(field: number, value: string): number[] {
  return protoBytes(field, [...Buffer.from(value)]);
}

const WINDOW: UsageSummaryInput = {
  timeZone: "UTC",
  sinceDay: UsageDay.make("2026-07-31"),
  untilDay: UsageDay.make("2026-08-02"),
};
// 2026-08-01T10:00:00Z, inside WINDOW.
const SECONDS = Date.parse("2026-08-01T10:00:00Z") / 1000;
const stamp = protoNumber(1, SECONDS);

// Distinct counters per row (k), optional variant (v) and id (field 11). Model 1026 = claude-opus-4-6.
const usage = (k: number, opts: { v?: number; id?: string } = {}) => {
  const v = opts.v ?? 0;
  return [
    ...protoNumber(1, 1026),
    ...protoNumber(2, k * 100 + 1 + v),
    ...protoNumber(3, k * 10 + 2 + v),
    ...protoNumber(4, k + 3 + v),
    ...protoNumber(5, k * 20 + 4 + v),
    ...(opts.id ? protoText(11, opts.id) : []),
  ];
};
const step = (main: number[], retries: number[][] = []) => [
  ...protoBytes(9, main),
  ...protoBytes(8, stamp),
  ...retries.flatMap((retry) => protoBytes(28, protoBytes(2, retry))),
];
const generation = (main: number[], retries: number[][] = []) =>
  protoBytes(1, [
    ...protoBytes(4, main),
    ...protoNumber(3, 1026),
    ...protoBytes(9, protoBytes(4, stamp)),
    ...retries.flatMap((retry) => protoBytes(17, protoBytes(2, retry))),
  ]);

interface Row {
  id: string;
  trueCalls: number;
  note: string;
  steps: [number, number[]][];
  generations: [number, number[]][];
}

const ROWS: Row[] = [
  {
    id: "R3",
    trueCalls: 1,
    note: "one call: step + generation + generation retry copy",
    steps: [[0, step(usage(3))]],
    generations: [[0, generation(usage(3), [usage(3)])]],
  },
  {
    id: "R6",
    trueCalls: 2,
    note: "id-less main + id-less retry w/ different counters, on step and generation",
    steps: [[0, step(usage(6), [usage(6, { v: 1 })])]],
    generations: [[0, generation(usage(6), [usage(6, { v: 1 })])]],
  },
  {
    id: "R7",
    trueCalls: 2,
    note: "id-less main + equal-counter retry with its own id (11), on step and generation",
    steps: [[0, step(usage(7), [usage(7, { id: "retry-7" })])]],
    generations: [[0, generation(usage(7), [usage(7, { id: "retry-7" })])]],
  },
  {
    id: "R8a",
    trueCalls: 1,
    note: "copies of one call: 2 step copies (separate steps) + 1 generation copy",
    steps: [
      [0, step(usage(8))],
      [1, step(usage(8))],
    ],
    generations: [[0, generation(usage(8))]],
  },
  {
    id: "R8b",
    trueCalls: 1,
    note: "copies of one call: 1 step copy + 2 generation copies",
    steps: [[0, step(usage(9))]],
    generations: [
      [0, generation(usage(9))],
      [1, generation(usage(9))],
    ],
  },
  {
    id: "R5",
    trueCalls: 2,
    note: "two genuine identical id-less calls, each stored 4x (step, step retry, gen, gen retry)",
    steps: [
      [0, step(usage(5), [usage(5)])],
      [1, step(usage(5), [usage(5)])],
    ],
    generations: [
      [0, generation(usage(5), [usage(5)])],
      [1, generation(usage(5), [usage(5)])],
    ],
  },
  {
    id: "R11",
    trueCalls: 2,
    note: "id-less main + id-less identical retry that was separately billed (indistinguishable from a duplicate)",
    steps: [[0, step(usage(11), [usage(11)])]],
    generations: [[0, generation(usage(11), [usage(11)])]],
  },
  {
    id: "R12",
    trueCalls: 2,
    note: "step A without generation + step B and generation B, equal counters+model",
    steps: [
      [0, step(usage(12))],
      [1, step(usage(12))],
    ],
    generations: [[1, generation(usage(12))]],
  },
];

const seed = (dir: string, row: Row) => {
  const db = new NodeSqlite.DatabaseSync(NodePath.join(dir, `${row.id}.db`));
  try {
    db.exec("CREATE TABLE gen_metadata (idx INTEGER, data BLOB)");
    db.exec("CREATE TABLE steps (idx INTEGER, metadata BLOB)");
    for (const [idx, bytes] of row.steps)
      db.prepare("INSERT INTO steps VALUES (?, ?)").run(idx, new Uint8Array(bytes));
    for (const [idx, bytes] of row.generations)
      db.prepare("INSERT INTO gen_metadata VALUES (?, ?)").run(idx, new Uint8Array(bytes));
  } finally {
    db.close();
  }
};

interface Totals {
  in: number;
  out: number;
  cacheCreate: number;
  cached: number;
}
const add = (
  sum: Totals,
  t: {
    uncachedInputTokens: number;
    outputTokens: number;
    cacheCreationTokens: number;
    cachedInputTokens: number;
  },
) => ({
  in: sum.in + t.uncachedInputTokens,
  out: sum.out + t.outputTokens,
  cacheCreate: sum.cacheCreate + t.cacheCreationTokens,
  cached: sum.cached + t.cachedInputTokens,
});
const zero: Totals = { in: 0, out: 0, cacheCreate: 0, cached: 0 };
const fmt = (t: Totals) => `${t.in}/${t.out}/${t.cacheCreate}/${t.cached}`;

const layerService = (home: string) =>
  Layer.fresh(CursorUsageAccounts.layer)
    .pipe(Layer.provide(CursorAccountReader.layer.pipe(Layer.provide(CursorKeychain.layer))))
    .pipe(
      Layer.provideMerge(AntigravityUsage.layer),
      Layer.provideMerge(ProviderHostLive.layer),
      Layer.provideMerge(CodexUsageHistory.layer.pipe(Layer.provideMerge(Sqlite.layerMemory))),
      Layer.provideMerge(Layer.mock(BackgroundPolicy.BackgroundPolicy)({})),
      Layer.provideMerge(Layer.mock(ServerSecretStore.ServerSecretStore)({})),
      Layer.provideMerge(ServerConfig.layerTest(process.cwd(), NodePath.join(home, "t3"))),
      Layer.provideMerge(NodeServices.layer),
      Layer.provideMerge(Layer.succeed(HostProcess.Platform, "linux")),
      Layer.provideMerge(
        ServerSettings.layerTest({
          providerInstances: {
            [ProviderInstanceId.make("claudeAgent")]: {
              driver: ProviderDriverKind.make("claudeAgent"),
              config: { homePath: NodePath.join(home, "claude") },
            },
            [ProviderInstanceId.make("codex")]: {
              driver: ProviderDriverKind.make("codex"),
              config: { homePath: NodePath.join(home, "codex") },
            },
          },
        }),
      ),
      Layer.provideMerge(
        Layer.succeed(
          HttpClient.HttpClient,
          HttpClient.make((request) =>
            Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({}))),
          ),
        ),
      ),
      Layer.provideMerge(
        Layer.succeed(HostProcess.Environment, {
          HOME: home,
          GROK_HOME: NodePath.join(home, "grok"),
          PI_CODING_AGENT_DIR: NodePath.join(home, "pi"),
          OPENCODE_DATA_DIR: NodePath.join(home, "opencode"),
          ANTIGRAVITY_DATA_DIR: NodePath.join(home, "antigravity"),
          XDG_CONFIG_HOME: NodePath.join(home, "config"),
          APPDATA: NodePath.join(home, "config"),
        }),
      ),
    );

describe("ev-23622 capture", () => {
  for (const row of ROWS) {
    it.live(`row ${row.id}`, () =>
      Effect.gen(function* () {
        const home = yield* Effect.promise(() =>
          NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), `ev23622-${row.id}-`)),
        );
        yield* Effect.addFinalizer(() =>
          Effect.promise(() => NodeFSP.rm(home, { recursive: true, force: true })),
        );
        const dir = NodePath.join(home, "antigravity");
        yield* Effect.promise(() => NodeFSP.mkdir(dir, { recursive: true }));
        seed(dir, row);

        const read = yield* readAntigravityUsage(dir, 0).pipe(Effect.provide(NodeServices.layer));
        assert.deepStrictEqual(read.errors, []);
        const records = read.files.flatMap((file) => file.records);
        const readerTotals = records.reduce((sum, r) => add(sum, r.totals), zero);

        const service = yield* UsageService.make.pipe(Effect.provide(layerService(home)));
        const summary = yield* service.readSummary(WINDOW);
        const buckets = summary.buckets.filter((bucket) => bucket.provider === "antigravity");
        const rpcTotals = buckets.reduce((sum, b) => add(sum, b.totals), zero);
        const rpcRecords = buckets.reduce((sum, b) => sum + b.records, 0);

        console.log(
          `ROW ${row.id} | true calls=${row.trueCalls} | reader records=${records.length} | reader totals in/out/cacheCreate/cached=${fmt(readerTotals)} | rpc totals in/out/cacheCreate/cached=${fmt(rpcTotals)} | rpc buckets=${buckets.length} | rpc bucket.records=${rpcRecords} | models=${[...new Set(records.map((r) => r.model))].join(",")} | ${row.note}`,
        );
      }).pipe(Effect.scoped),
    );
  }
});
