// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: GPL-3.0-or-later

import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { AgentError } from "../src/util/errors.js";
import { expandEnvVars, resolveConfigValue, resolveHeaders } from "../src/util/env.js";
import { buildGetApiKey } from "../src/agent/runner.js";
import type { RumiloConfig } from "../src/config/schema.js";

const stubConfig: RumiloConfig = {
  defaults: { model: "anthropic:test", cleanup: true },
  web: { model: "anthropic:test" },
  repo: { model: "anthropic:test", default_depth: 1, blob_limit: "5m" },
};

function customModel(provider: string, apiKey?: string): RumiloConfig {
  return {
    ...stubConfig,
    custom_models: {
      mymodel: {
        id: "m1",
        name: "M1",
        api: "openai-completions" as any,
        provider,
        base_url: "http://localhost:8000/v1",
        reasoning: false,
        input: ["text"],
        cost: { input: 0, output: 0 },
        context_window: 8192,
        max_tokens: 4096,
        ...(apiKey ? { api_key: apiKey } : {}),
      },
    },
  };
}

describe("AgentError", () => {
  test("has correct name, code, and inherits from Error", () => {
    const err = new AgentError("boom");
    expect(err).toBeInstanceOf(Error);
    expect(err.name).toBe("AgentError");
    expect(err.code).toBe("AGENT_ERROR");
    expect(err.message).toBe("boom");
  });
});

describe("resolveConfigValue", () => {
  const saved: Record<string, string | undefined> = {};

  beforeAll(() => {
    saved["RUMILO_TEST_KEY"] = process.env["RUMILO_TEST_KEY"];
    process.env["RUMILO_TEST_KEY"] = "resolved-value";
  });

  afterAll(() => {
    if (saved["RUMILO_TEST_KEY"] === undefined) delete process.env["RUMILO_TEST_KEY"];
    else process.env["RUMILO_TEST_KEY"] = saved["RUMILO_TEST_KEY"];
  });

  test("resolves bare env var name", () => {
    expect(resolveConfigValue("RUMILO_TEST_KEY")).toBe("resolved-value");
  });

  test("resolves $VAR reference", () => {
    expect(resolveConfigValue("$RUMILO_TEST_KEY")).toBe("resolved-value");
  });

  test("resolves ${VAR} reference", () => {
    expect(resolveConfigValue("${RUMILO_TEST_KEY}")).toBe("resolved-value");
  });

  test("treats unknown name as literal", () => {
    expect(resolveConfigValue("sk-literal-key-12345")).toBe("sk-literal-key-12345");
  });

  test("returns undefined for unknown $VAR", () => {
    expect(resolveConfigValue("$RUMILO_NONEXISTENT_XYZ")).toBeUndefined();
  });

  test("executes shell commands with ! prefix", () => {
    expect(resolveConfigValue("!echo hello")).toBe("hello");
  });

  test("returns undefined for failing shell command", () => {
    expect(resolveConfigValue("!false")).toBeUndefined();
  });
});

describe("expandEnvVars", () => {
  const saved: Record<string, string | undefined> = {};

  beforeAll(() => {
    saved["FOO"] = process.env["FOO"];
    saved["BAR"] = process.env["BAR"];
    process.env["FOO"] = "hello";
    process.env["BAR"] = "world";
  });

  afterAll(() => {
    if (saved["FOO"] === undefined) delete process.env["FOO"];
    else process.env["FOO"] = saved["FOO"];
    if (saved["BAR"] === undefined) delete process.env["BAR"];
    else process.env["BAR"] = saved["BAR"];
  });

  test("expands $VAR", () => {
    expect(expandEnvVars("Bearer $FOO")).toBe("Bearer hello");
  });

  test("expands ${VAR}", () => {
    expect(expandEnvVars("Bearer ${FOO}")).toBe("Bearer hello");
  });

  test("expands multiple vars", () => {
    expect(expandEnvVars("$FOO-$BAR")).toBe("hello-world");
  });

  test("missing var becomes empty string", () => {
    expect(expandEnvVars("key=$NONEXISTENT_RUMILO_VAR_XYZ")).toBe("key=");
  });

  test("string without vars is unchanged", () => {
    expect(expandEnvVars("plain text")).toBe("plain text");
  });
});

describe("resolveHeaders", () => {
  const saved: Record<string, string | undefined> = {};

  beforeAll(() => {
    saved["RUMILO_HDR_KEY"] = process.env["RUMILO_HDR_KEY"];
    process.env["RUMILO_HDR_KEY"] = "hdr-value";
  });

  afterAll(() => {
    if (saved["RUMILO_HDR_KEY"] === undefined) delete process.env["RUMILO_HDR_KEY"];
    else process.env["RUMILO_HDR_KEY"] = saved["RUMILO_HDR_KEY"];
  });

  test("returns undefined for undefined input", () => {
    expect(resolveHeaders(undefined)).toBeUndefined();
  });

  test("resolves header values via resolveConfigValue", () => {
    const result = resolveHeaders({ "X-Key": "RUMILO_HDR_KEY" });
    expect(result).toEqual({ "X-Key": "hdr-value" });
  });

  test("drops entries that resolve to undefined", () => {
    const result = resolveHeaders({ "X-Key": "$RUMILO_NONEXISTENT_XYZ" });
    expect(result).toBeUndefined();
  });
});

describe("buildGetApiKey", () => {
  const saved: Record<string, string | undefined> = {};

  beforeAll(() => {
    saved["ANTHROPIC_API_KEY"] = process.env["ANTHROPIC_API_KEY"];
    saved["CUSTOM_KEY"] = process.env["CUSTOM_KEY"];
    process.env["ANTHROPIC_API_KEY"] = "sk-ant-test";
    process.env["CUSTOM_KEY"] = "sk-custom-test";
  });

  afterAll(() => {
    for (const [k, v] of Object.entries(saved)) {
      if (v === undefined) delete process.env[k];
      else process.env[k] = v;
    }
  });

  test("falls back to pi-ai env var lookup for built-in providers", () => {
    const getKey = buildGetApiKey(stubConfig);
    expect(getKey("anthropic")).toBe("sk-ant-test");
  });

  test("returns undefined for unknown provider with no config", () => {
    const getKey = buildGetApiKey(stubConfig);
    expect(getKey("unknown-provider")).toBeUndefined();
  });

  test("resolves literal api_key from custom model", () => {
    const config = customModel("myprovider", "sk-literal-key");
    const getKey = buildGetApiKey(config);
    expect(getKey("myprovider")).toBe("sk-literal-key");
  });

  test("resolves api_key via env var name", () => {
    const config = customModel("myprovider", "CUSTOM_KEY");
    const getKey = buildGetApiKey(config);
    expect(getKey("myprovider")).toBe("sk-custom-test");
  });

  test("resolves api_key via $VAR reference", () => {
    const config = customModel("myprovider", "$CUSTOM_KEY");
    const getKey = buildGetApiKey(config);
    expect(getKey("myprovider")).toBe("sk-custom-test");
  });

  test("resolves api_key via shell command", () => {
    const config = customModel("myprovider", "!echo shell-key");
    const getKey = buildGetApiKey(config);
    expect(getKey("myprovider")).toBe("shell-key");
  });

  test("custom model provider doesn't shadow built-in provider lookup", () => {
    const config = customModel("other-provider", "sk-other");
    const getKey = buildGetApiKey(config);
    expect(getKey("anthropic")).toBe("sk-ant-test");
  });

  test("falls back to env var lookup when custom model has no api_key", () => {
    const config = customModel("anthropic");
    const getKey = buildGetApiKey(config);
    expect(getKey("anthropic")).toBe("sk-ant-test");
  });
});
