/**
 * Search provider 單元測試
 * 執行：npm run test:search
 */
import assert from "node:assert/strict";
import { clearSearchCacheForTests } from "./cache";
import { LEGACY_SEARCH_ENV_KEYS } from "./config";
import {
  dedupeKeyForResult,
  dedupeResults,
  filterNotifyDuplicates,
  isNotifyDuplicate,
  markNotified,
  normalizeThreadsUrl,
} from "./dedupe";
import { executeBraveSearch } from "./brave-execute";
import { evaluateNotifyRules, filterNotifiableResults } from "./notify-rules";
import { runOrchestratedSearch } from "./orchestrator-search";
import { resetQuotaForTests } from "./quota";
import {
  modeAllowsBrave,
  modeAllowsCrawler,
  modeAllowsThreads,
} from "./source-mode";
import type { SearchProvider, SearchResponse, SearchResult } from "./types";

let passed = 0;
let failed = 0;

function test(name: string, fn: () => void | Promise<void>) {
  return (async () => {
    try {
      await fn();
      passed++;
      console.log(`  ✓ ${name}`);
    } catch (error) {
      failed++;
      console.error(`  ✗ ${name}`);
      console.error(`    ${error instanceof Error ? error.message : error}`);
    }
  })();
}

function mockProvider(
  name: "threads" | "brave" | "crawler",
  results: SearchResult[],
  enabled = true
): SearchProvider & { getCallCount: () => number } {
  let calls = 0;
  return {
    name: () => name,
    enabled: () => enabled,
    search: async (): Promise<SearchResponse> => {
      calls++;
      return { provider: name, results, status: "success" };
    },
    getCallCount: () => calls,
  };
}

async function run() {
  console.log("\n=== Search Provider Tests ===\n");

  await test("Threads 成功時不呼叫 Brave", async () => {
    const threads = mockProvider("threads", [
      { title: "t", url: "https://threads.com/@a/post/1", snippet: "s", author: "a", source: "threads" },
    ]);
    const brave = mockProvider("brave", [
      { title: "b", url: "https://threads.com/@b/post/2", snippet: "s", author: "b", source: "brave" },
    ]);
    const crawler = mockProvider("crawler", []);

    const out = await runOrchestratedSearch(
      { threads, brave, crawler },
      { value: "狗洗澡", limit: 10, priority: "high" }
    );

    assert.equal(out.providersUsed.join(","), "threads");
    assert.equal(brave.getCallCount(), 0);
  });

  await test("Threads 失敗時 high priority 會 fallback Brave", async () => {
    const threads = mockProvider("threads", []);
    const brave = mockProvider("brave", [
      { title: "b", url: "https://threads.com/@b/post/2", snippet: "s", author: "b", source: "brave", threadId: "2" },
    ]);
    const crawler = mockProvider("crawler", []);

    const out = await runOrchestratedSearch(
      { threads, brave, crawler },
      { value: "狗洗澡", limit: 10, priority: "high" },
      { skipCrawler: true }
    );

    assert.ok(out.providersUsed.includes("brave"));
    assert.equal(out.results.length, 1);
  });

  await test("medium priority 不會使用 Brave", async () => {
    const threads = mockProvider("threads", []);
    let braveCalled = false;
    const brave: SearchProvider = {
      name: () => "brave",
      enabled: () => true,
      search: async () => {
        braveCalled = true;
        return { provider: "brave", results: [], status: "success" };
      },
    };

    await runOrchestratedSearch(
      { threads, brave, crawler: mockProvider("crawler", []) },
      { value: "狗洗澡", limit: 10, priority: "medium" },
      { skipCrawler: true }
    );
    assert.equal(braveCalled, false);
  });

  await test("low priority 不會使用 Brave", async () => {
    let braveCalled = false;
    const brave: SearchProvider = {
      name: () => "brave",
      enabled: () => true,
      search: async () => {
        braveCalled = true;
        return { provider: "brave", results: [], status: "success" };
      },
    };

    await runOrchestratedSearch(
      {
        threads: mockProvider("threads", []),
        brave,
        crawler: mockProvider("crawler", []),
      },
      { value: "狗洗澡", limit: 10, priority: "low" },
      { skipCrawler: true }
    );
    assert.equal(braveCalled, false);
  });

  await test("Brave 超過每日額度時不呼叫 API", async () => {
    clearSearchCacheForTests();
    resetQuotaForTests();

    process.env.BRAVE_SEARCH_ENABLED = "true";
    process.env.BRAVE_SEARCH_API_KEY = "test-key";
    process.env.BRAVE_DAILY_LIMIT = "1";

    let fetchCalled = false;
    const originalFetch = globalThis.fetch;
    globalThis.fetch = async () => {
      fetchCalled = true;
      return new Response(JSON.stringify({ web: { results: [] } }), { status: 200 });
    };

    try {
      await executeBraveSearch({
        keyword: "first",
        limit: 5,
        priority: "high",
        patrolMode: true,
      });
      const second = await executeBraveSearch({
        keyword: "second",
        limit: 5,
        priority: "high",
        patrolMode: true,
      });
      assert.equal(second.status, "skipped");
      assert.equal(second.skipReason, "daily_limit_reached");
      assert.equal(fetchCalled, true);
    } finally {
      globalThis.fetch = originalFetch;
      delete process.env.BRAVE_DAILY_LIMIT;
    }
  });

  await test("Brave query cache 命中時不呼叫 API", async () => {
    clearSearchCacheForTests();
    resetQuotaForTests();

    process.env.BRAVE_SEARCH_ENABLED = "true";
    process.env.BRAVE_SEARCH_API_KEY = "test-key";
    process.env.BRAVE_DAILY_LIMIT = "30";

    let fetchCount = 0;
    const originalFetch = globalThis.fetch;
    globalThis.fetch = async () => {
      fetchCount++;
      return new Response(
        JSON.stringify({
          web: {
            results: [
              {
                title: "t",
                description: "d",
                url: "https://www.threads.com/@u/post/abc",
              },
            ],
          },
        }),
        { status: 200 }
      );
    };

    try {
      await executeBraveSearch({
        keyword: "cache-test",
        query: 'site:threads.com "cache-test"',
        limit: 5,
        priority: "high",
        patrolMode: true,
      });
      await executeBraveSearch({
        keyword: "cache-test",
        query: 'site:threads.com "cache-test"',
        limit: 5,
        priority: "high",
        patrolMode: true,
      });
      assert.equal(fetchCount, 1);
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  await test("相同 thread_id 會被 dedupe", () => {
    const results: SearchResult[] = [
      {
        title: "a",
        url: "https://threads.com/@u/post/same123",
        snippet: "1",
        author: "u",
        source: "threads",
        threadId: "same123",
      },
      {
        title: "b",
        url: "https://www.threads.com/@u/post/same123/",
        snippet: "2",
        author: "u",
        source: "brave",
      },
    ];
    const deduped = dedupeResults(results);
    assert.equal(deduped.length, 1);
    assert.equal(dedupeKeyForResult(results[0]), "thread:same123");
  });

  await test("相同 normalized_url 會被 dedupe", () => {
    const urlA = "https://www.threads.com/@u/post/x?utm=1#frag";
    const urlB = "https://threads.com/@u/post/x";
    assert.equal(normalizeThreadsUrl(urlA), normalizeThreadsUrl(urlB));

    const results: SearchResult[] = [
      { title: "a", url: urlA, snippet: "1", author: "u", source: "brave" },
      { title: "b", url: urlB, snippet: "2", author: "u", source: "brave" },
    ];
    assert.equal(dedupeResults(results).length, 1);
  });

  await test("未達通知標準不通知", () => {
    const result: SearchResult = {
      title: "今天天氣真好",
      url: "https://threads.com/@u/post/1",
      snippet: "心情不錯",
      author: "u",
      source: "threads",
    };
    const verdict = evaluateNotifyRules(result, { brandTerms: ["超級品牌"] });
    assert.equal(verdict.matched, false);
    assert.equal(filterNotifiableResults([result], { brandTerms: ["超級品牌"] }).length, 0);
  });

  await test("命中負面詞會通知", () => {
    const result: SearchResult = {
      title: "這產品踩雷了",
      url: "https://threads.com/@u/post/2",
      snippet: "真的很失望",
      author: "u",
      source: "brave",
    };
    const matched = filterNotifiableResults([result], {});
    assert.equal(matched.length, 1);
    assert.ok(matched[0].matchedRules.includes("negative_word"));
  });

  await test("舊 provider 環境變數已標記 deprecated", () => {
    const expected = [
      "SERPAPI_API_KEY",
      "SERPER_API_KEY",
      "GOOGLE_SEARCH_API_KEY",
      "TAVILY_API_KEY",
      "EXA_API_KEY",
      "SEARXNG_BASE_URL",
      "DUCKDUCKGO_ENABLED",
    ];
    for (const key of expected) {
      assert.ok(
        (LEGACY_SEARCH_ENV_KEYS as readonly string[]).includes(key),
        `missing legacy key ${key}`
      );
    }
  });

  await test("threads 模式不允許 Brave / 爬蟲", () => {
    assert.equal(modeAllowsThreads("threads"), true);
    assert.equal(modeAllowsBrave("threads"), false);
    assert.equal(modeAllowsCrawler("threads"), false);
  });

  await test("mixed 模式允許三種來源", () => {
    assert.equal(modeAllowsThreads("mixed"), true);
    assert.equal(modeAllowsBrave("mixed"), true);
    assert.equal(modeAllowsCrawler("mixed"), true);
  });

  await test("24h notify dedupe 會跳過重複", () => {
    clearSearchCacheForTests();
    const result: SearchResult = {
      title: "踩雷",
      url: "https://threads.com/@u/post/dedupe1",
      snippet: "不推",
      author: "u",
      source: "brave",
      threadId: "dedupe1",
    };
    const key = dedupeKeyForResult(result);
    markNotified(key, 60_000);
    assert.equal(isNotifyDuplicate(key), true);
    assert.equal(filterNotifyDuplicates([result]).length, 0);
  });

  console.log(`\n${passed} passed, ${failed} failed\n`);
  if (failed > 0) process.exit(1);
}

run();
