import assert from "node:assert/strict";
import test from "node:test";
import type { TrendDecision } from "../src/providers/model.js";
import type { AgentConfig } from "../src/config.js";
import type { TrendResearch } from "../src/providers/xapi.js";
import { applyDecisionPolicy } from "../src/scan.js";
import { analyzeTrends, modelEndpoint } from "../src/providers/model.js";

const targetAddress = "0x1111111111111111111111111111111111111111";

const decision = (overrides: Partial<TrendDecision> = {}): TrendDecision => ({
  topic: "#Alpha",
  summary: "An emerging topic supported by public posts.",
  score: 90,
  stage: "emerging",
  manipulationRisk: "low",
  legalRisk: "low",
  recommendLaunch: true,
  evidence: ["public post"],
  token: { name: "Alpha", symbol: "ALPHA", description: "Alpha topic token", reason: "emerging discussion" },
  buybackTarget: { address: targetAddress, reason: "address appears in a public post", confidence: 90 },
  ...overrides,
});

const research = (searchResponse: unknown): TrendResearch[] => [{
  name: "#Alpha",
  rank: 1,
  searchResponse,
}];

test("buyback address must be present in the supplied X research", () => {
  const approved = applyDecisionPolicy({ minLaunchScore: 80, minBuybackScore: 80, aggressiveLaunchMode: false }, research({ text: `token ${targetAddress}` }), [decision()])[0];
  assert.equal(approved?.eligible, true);
  assert.equal(approved?.buybackEligible, true);

  const rejected = applyDecisionPolicy({ minLaunchScore: 80, minBuybackScore: 80, aggressiveLaunchMode: false }, research({ text: "no contract address" }), [decision()])[0];
  assert.equal(rejected?.buybackEligible, false);
  assert.match(rejected?.buybackBlockedReasons.join(" ") ?? "", /not present in X research data/i);
});

test("a model cannot authorize launch for a topic absent from the X candidates", () => {
  const result = applyDecisionPolicy({ minLaunchScore: 80, minBuybackScore: 80, aggressiveLaunchMode: false }, research({ text: targetAddress }), [decision({ topic: "invented topic" })])[0];
  assert.equal(result?.eligible, false);
  assert.match(result?.blockedReasons.join(" ") ?? "", /not present in the X research candidates/i);
});

test("aggressive launch mode accepts peak medium-risk candidates but still rejects high risk", () => {
  const config = { minLaunchScore: 10, minBuybackScore: 30, aggressiveLaunchMode: true };
  const accepted = applyDecisionPolicy(config, research({ text: targetAddress }), [decision({
    score: 10,
    stage: "peak",
    manipulationRisk: "medium",
    legalRisk: "medium",
    recommendLaunch: false,
  })])[0];
  assert.equal(accepted?.eligible, true);

  const rejected = applyDecisionPolicy(config, research({ text: targetAddress }), [decision({
    score: 10,
    manipulationRisk: "high",
  })])[0];
  assert.equal(rejected?.eligible, false);
  assert.match(rejected?.blockedReasons.join(" ") ?? "", /manipulation risk is high/i);
});

test("model gateway accepts host, v1 base, and full chat-completions URLs", () => {
  assert.equal(modelEndpoint("https://gateway.example").href, "https://gateway.example/v1/chat/completions");
  assert.equal(modelEndpoint("https://gateway.example/v1").href, "https://gateway.example/v1/chat/completions");
  assert.equal(modelEndpoint("https://gateway.example/v1/chat/completions").href, "https://gateway.example/v1/chat/completions");
});

test("model analysis retries once when the first JSON response violates the decision contract", async () => {
  const originalFetch = globalThis.fetch;
  let calls = 0;
  globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => {
    calls += 1;
    const body = JSON.parse(String(init?.body)) as { messages: Array<{ role: string; content: string }> };
    if (calls === 2) assert.match(body.messages.at(-1)?.content ?? "", /failed validation/i);
    const content = calls === 1
      ? JSON.stringify({ decisions: [{ analysis: "missing required fields" }] })
      : JSON.stringify({ decisions: [decision({ buybackTarget: null })] });
    return new Response(JSON.stringify({ choices: [{ message: { content } }] }), {
      status: 200,
      headers: { "content-type": "application/json" },
    });
  }) as typeof fetch;

  try {
    const config = {
      model: { baseUrl: "https://gateway.example/v1", apiKey: "test-key", name: "test-model" },
      trendMaxCandidates: 5,
    } as AgentConfig;
    const result = await analyzeTrends(config, research({ data: { tweets: [] } }));
    assert.equal(calls, 2);
    assert.equal(result[0]?.topic, "#Alpha");
  } finally {
    globalThis.fetch = originalFetch;
  }
});

test("model input keeps the useful tweet fields and bounds payload size", async (context) => {
  let requestBody: { messages: Array<{ role: string; content: string }> } | undefined;
  context.mock.method(globalThis, "fetch", async (_input, init) => {
    requestBody = JSON.parse(String(init?.body)) as typeof requestBody;
    return new Response(JSON.stringify({
      choices: [{ message: { content: JSON.stringify({ decisions: [decision({ buybackTarget: null })] }) } }],
    }), { status: 200, headers: { "content-type": "application/json" } });
  });

  const config = {
    model: { baseUrl: "https://gateway.example/v1", apiKey: "test-key", name: "test-model" },
    trendMaxCandidates: 5,
  } as AgentConfig;
  const tweets = Array.from({ length: 30 }, (_, index) => ({
    tweet_id: String(index),
    text: `#Alpha evidence ${index}`,
    view_count: index,
    user: { screen_name: `user${index}`, followers_count: index, description: "unused profile data" },
  }));
  await analyzeTrends(config, research({ data: { tweets } }));

  const prompt = JSON.parse(requestBody?.messages[1]?.content ?? "{}") as {
    candidates?: Array<{ searchResponse?: { tweets?: unknown[] } }>;
  };
  assert.equal(prompt.candidates?.[0]?.searchResponse?.tweets?.length, 20);
  assert.doesNotMatch(requestBody?.messages[1]?.content ?? "", /unused profile data/);
});
