/**
 * T-03 provider-hook contract tests.
 *
 * Covers `createOmniRouteProviderHook(opts, deps)`:
 *   - hook.id binds to resolved providerId (single + multi-instance)
 *   - models() narrows ctx.auth, fetches via injected fetcher, caches per
 *     (baseURL, apiKey) tuple, refetches after TTL
 *   - mapRawModelToModelV2 emits a v2 Model shape matching the
 *     @opencode-ai/sdk/v2 type
 *
 * Mocking strategy: the fetcher is dependency-injected at hook construction
 * (`deps.fetcher`). No global fetch monkey-patch needed. `deps.now` lets us
 * fast-forward time deterministically for TTL assertions.
 */

import test from "node:test";
import assert from "node:assert/strict";
import {
  createOmniRouteProviderHook,
  mapRawModelToModelV2,
  type OmniRouteRawModelEntry,
  type OmniRouteModelsFetcher,
} from "../src/index.js";

const FIXTURE: OmniRouteRawModelEntry[] = [
  {
    id: "claude-primary",
    object: "model",
    owned_by: "combo",
    capabilities: { tool_calling: true, reasoning: true, vision: true, thinking: true },
    context_length: 200000,
    max_output_tokens: 64000,
    input_modalities: ["text", "image"],
    output_modalities: ["text"],
  },
  {
    id: "claude-low",
    object: "model",
    owned_by: "combo",
    capabilities: { tool_calling: true, reasoning: true, vision: true, thinking: false },
    context_length: 200000,
    max_output_tokens: 64000,
    input_modalities: ["text", "image"],
    output_modalities: ["text"],
  },
  {
    id: "gemini-3-flash",
    object: "model",
    owned_by: "google",
    capabilities: { tool_calling: true, reasoning: false, vision: true, thinking: false },
    context_length: 1000000,
    max_output_tokens: 8192,
    input_modalities: ["text", "image"],
    output_modalities: ["text"],
  },
];

function stubFetcher(payload: OmniRouteRawModelEntry[]): OmniRouteModelsFetcher & {
  callCount: () => number;
  callsBy: () => Array<[string, string]>;
} {
  let calls: Array<[string, string]> = [];
  const f: OmniRouteModelsFetcher = async (baseURL, apiKey) => {
    calls.push([baseURL, apiKey]);
    return payload;
  };
  return Object.assign(f, {
    callCount: () => calls.length,
    callsBy: () => calls,
  });
}

const apiAuth = (key: string, baseURL?: string): unknown =>
  baseURL ? { type: "api", key, baseURL } : { type: "api", key };

test("createOmniRouteProviderHook: default providerId is 'omniroute'", () => {
  const hook = createOmniRouteProviderHook(undefined, { combosFetcher: async () => [] });
  assert.equal(hook.id, "omniroute");
});

test("createOmniRouteProviderHook: custom providerId binds to hook.id (multi-instance)", () => {
  const a = createOmniRouteProviderHook(
    { providerId: "omniroute-preprod" },
    { combosFetcher: async () => [] }
  );
  const b = createOmniRouteProviderHook(
    { providerId: "omniroute-local" },
    { combosFetcher: async () => [] }
  );
  assert.equal(a.id, "omniroute-preprod");
  assert.equal(b.id, "omniroute-local");
});

test("models: extracts apiKey from ctx.auth (type=api) and calls fetcher with it", async () => {
  const fetcher = stubFetcher(FIXTURE);
  const hook = createOmniRouteProviderHook(
    { baseURL: "https://or.example.com/v1" },
    { fetcher, combosFetcher: async () => [] }
  );
  const out = await hook.models!({} as never, { auth: apiAuth("sk-abc") as never });
  assert.equal(fetcher.callCount(), 1);
  assert.deepEqual(fetcher.callsBy()[0], ["https://or.example.com/v1", "sk-abc"]);
  assert.equal(Object.keys(out).length, 3);
  assert.ok(out["claude-primary"]);
});

test("models: returns {} when ctx.auth is null/undefined/wrong-type/empty-key", async () => {
  const fetcher = stubFetcher(FIXTURE);
  const hook = createOmniRouteProviderHook(
    { baseURL: "https://or.example.com/v1" },
    { fetcher, combosFetcher: async () => [] }
  );

  assert.deepEqual(await hook.models!({} as never, {} as never), {});
  assert.deepEqual(await hook.models!({} as never, { auth: undefined } as never), {});
  assert.deepEqual(
    await hook.models!({} as never, {
      auth: { type: "oauth", refresh: "r", access: "a", expires: 0 } as never,
    }),
    {}
  );
  assert.deepEqual(
    await hook.models!({} as never, { auth: { type: "api", key: "" } as never }),
    {}
  );
  assert.equal(fetcher.callCount(), 0, "fetcher must not be called on auth rejection");
});

test("models: returns {} when no baseURL resolvable (no opts.baseURL and no auth.baseURL)", async () => {
  const fetcher = stubFetcher(FIXTURE);
  const hook = createOmniRouteProviderHook({}, { fetcher, combosFetcher: async () => [] });
  // valid api auth but neither opts nor auth carries a baseURL
  assert.deepEqual(await hook.models!({} as never, { auth: apiAuth("sk-x") as never }), {});
  assert.equal(fetcher.callCount(), 0);
});

test("models: baseURL falls back to auth.baseURL when opts.baseURL absent", async () => {
  const fetcher = stubFetcher(FIXTURE);
  const hook = createOmniRouteProviderHook({}, { fetcher, combosFetcher: async () => [] });
  const out = await hook.models!({} as never, {
    auth: apiAuth("sk-y", "https://or.creds-attached.example/v1") as never,
  });
  assert.equal(fetcher.callCount(), 1);
  assert.equal(fetcher.callsBy()[0][0], "https://or.creds-attached.example/v1");
  assert.equal(Object.keys(out).length, 3);
});

test("models: maps a sample /v1/models entry to ModelV2 (sanity)", async () => {
  const fetcher = stubFetcher(FIXTURE);
  const hook = createOmniRouteProviderHook(
    { providerId: "omniroute", baseURL: "https://or.example.com/v1" },
    { fetcher, combosFetcher: async () => [] }
  );
  const out = await hook.models!({} as never, { auth: apiAuth("sk-abc") as never });
  const claude = out["claude-primary"];
  assert.ok(claude, "claude-primary present");
  assert.equal(claude.id, "claude-primary");
  assert.equal(claude.name, "claude-primary");
  assert.equal(claude.providerID, "omniroute");
  assert.equal(claude.api.id, "openai-compatible");
  assert.equal(claude.api.url, "https://or.example.com/v1");
  assert.equal(claude.api.npm, "@ai-sdk/openai-compatible");
  // capabilities: toolcall (one word), reasoning OR thinking, attachment = vision
  assert.equal(claude.capabilities.toolcall, true);
  assert.equal(claude.capabilities.reasoning, true);
  assert.equal(claude.capabilities.attachment, true);
  assert.equal(claude.capabilities.temperature, true);
  // modalities mapped from arrays
  assert.equal(claude.capabilities.input.text, true);
  assert.equal(claude.capabilities.input.image, true);
  assert.equal(claude.capabilities.input.audio, false);
  assert.equal(claude.capabilities.output.text, true);
  assert.equal(claude.capabilities.output.image, false);
  // cost is zeroed (OmniRoute /v1/models has no pricing)
  assert.deepEqual(claude.cost, { input: 0, output: 0, cache: { read: 0, write: 0 } });
  // limits
  assert.equal(claude.limit.context, 200000);
  assert.equal(claude.limit.output, 64000);
  assert.equal(claude.status, "active");
});

test("mapRawModelToModelV2: thinking-only model still surfaces reasoning=true", () => {
  const m = mapRawModelToModelV2(
    {
      id: "thinking-only",
      capabilities: { thinking: true, reasoning: false },
      context_length: 100000,
      max_output_tokens: 8192,
    },
    { providerId: "omniroute", baseURL: "https://or.example.com/v1" }
  );
  assert.equal(m.capabilities.reasoning, true);
});

test("mapRawModelToModelV2: missing capabilities defaults to all-false (except temperature)", () => {
  const m = mapRawModelToModelV2(
    { id: "minimal" },
    { providerId: "omniroute", baseURL: "https://or.example.com/v1" }
  );
  assert.equal(m.capabilities.temperature, true);
  assert.equal(m.capabilities.reasoning, false);
  assert.equal(m.capabilities.attachment, false);
  assert.equal(m.capabilities.toolcall, false);
  // default modalities = text only
  assert.equal(m.capabilities.input.text, true);
  assert.equal(m.capabilities.output.text, true);
  // missing context / output tokens → 0 fallback (ModelV2.limit.{context,output} required)
  assert.equal(m.limit.context, 0);
  assert.equal(m.limit.output, 0);
});

test("models: caches result for second call within TTL (fetcher called once)", async () => {
  const fetcher = stubFetcher(FIXTURE);
  let nowMs = 1_000_000;
  const hook = createOmniRouteProviderHook(
    { baseURL: "https://or.example.com/v1", modelCacheTtl: 60_000 },
    { fetcher, now: () => nowMs, combosFetcher: async () => [] }
  );

  const a = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
  nowMs += 30_000; // half the TTL
  const b = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
  assert.equal(fetcher.callCount(), 1, "second call within TTL must hit the cache");
  assert.equal(Object.keys(a).length, 3);
  assert.equal(Object.keys(b).length, 3);
});

test("models: refetches after TTL expires", async () => {
  const fetcher = stubFetcher(FIXTURE);
  let nowMs = 1_000_000;
  const hook = createOmniRouteProviderHook(
    { baseURL: "https://or.example.com/v1", modelCacheTtl: 60_000 },
    { fetcher, now: () => nowMs, combosFetcher: async () => [] }
  );

  await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
  nowMs += 60_001; // just past the TTL
  await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
  assert.equal(fetcher.callCount(), 2, "call past TTL must refetch");
});

test("models: caches per (baseURL, apiKey) tuple (different keys → independent fetches)", async () => {
  const fetcher = stubFetcher(FIXTURE);
  const hook = createOmniRouteProviderHook(
    { baseURL: "https://or.example.com/v1", modelCacheTtl: 300_000 },
    { fetcher, combosFetcher: async () => [] }
  );

  await hook.models!({} as never, { auth: apiAuth("sk-A") as never });
  await hook.models!({} as never, { auth: apiAuth("sk-B") as never });
  await hook.models!({} as never, { auth: apiAuth("sk-A") as never }); // cached
  await hook.models!({} as never, { auth: apiAuth("sk-B") as never }); // cached
  assert.equal(fetcher.callCount(), 2, "one fetch per distinct apiKey, then cache hits");
});

test("models: caches per (baseURL, apiKey) tuple (different baseURL → independent fetches)", async () => {
  const fetcher = stubFetcher(FIXTURE);
  const hook = createOmniRouteProviderHook(
    { modelCacheTtl: 300_000 }, // no opts.baseURL → falls back to auth.baseURL
    { fetcher, combosFetcher: async () => [] }
  );

  await hook.models!({} as never, { auth: apiAuth("sk-same", "https://prod.example/v1") as never });
  await hook.models!({} as never, {
    auth: apiAuth("sk-same", "https://preprod.example/v1") as never,
  });
  await hook.models!({} as never, { auth: apiAuth("sk-same", "https://prod.example/v1") as never }); // cached
  assert.equal(fetcher.callCount(), 2, "distinct baseURLs share apiKey but not cache");
});
