/**
 * TelosDB MCP クライアントテスト。
 * ビルドバージョン（package.json）と API /version（Cargo.toml 由来）の一致も検証する。
 */
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PKG = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../package.json"), "utf8"));

/**
 * 前提: MCP が 127.0.0.1:3001 で起動していること（npm run test-and-heal または test-and-heal:pro で起動＋実行される）。
 *
 * テスト内容:
 * [1] tools/list
 * [2] search_text
 * [3] Pro: スコアが全件 0.4 でないこと（警告のみの緩いチェック）
 * [4] Pro: ベクトル化 E2E — RE-INDEX 実行 → indexing_status が idle になるまで待機 → 検索 → vector_search_used が true かつスコアが全件 0.4 でないことを検証（ここで失敗すればベクトル化が効いていない）
 * [5] get_item_by_id（不正 id で isError）
 * [6] get_document_count
 * [7] add_item_text → list_documents → get_document → get_item_by_id → update_item → delete_item → delete_document（CRUD 一連）
 */
import axios from 'axios';

const API_BASE = "http://127.0.0.1:3001";

async function postMessage(axiosInstance, method, params = {}) {
    const res = await axiosInstance.post("/messages", {
        jsonrpc: "2.0",
        method: method,
        params: params,
        id: Date.now()
    });
    return res.data;
}

/**
 * Pro 時: 複数ヒットで全件スコア 0.4 なら LIKE フォールバックのみ＝近似近傍が動いていない欠陥を検出する。
 * strict が true のときは失敗とする。false のときは警告のみ（既存データに依存するため）。
 */
async function assertProVectorScoresNotAllFallback(axiosInstance, strict = false) {
    const editionRes = await axiosInstance.get("/edition");
    if (editionRes.data?.edition !== "pro") return;

    const searchResult = await postMessage(axiosInstance, "tools/call", {
        name: "search_text",
        arguments: { content: "文", limit: 5 }
    });
    const raw = searchResult.result?.content?.[0]?.text;
    if (!raw) throw new Error("Pro vector check: no search response");
    let parsed;
    try {
        parsed = JSON.parse(raw);
    } catch {
        throw new Error("Pro vector check: search result is not JSON");
    }
    const items = Array.isArray(parsed) ? parsed : (parsed?.items ?? []);
    if (!Array.isArray(items) || items.length < 2) {
        if (strict) throw new Error("Pro vector check: need at least 2 docs to assert scores");
        return;
    }

    const likeFallbackScore = 0.4;
    const allFallback = items.every(
        (it) => Math.abs((it.similarity ?? 0) - likeFallbackScore) < 1e-5
    );
    if (allFallback) {
        const msg = "Pro: 全件スコア 0.4（キーワード一致のみ）。RE-INDEX でベクトル化してください。";
        if (strict) throw new Error(msg);
        console.warn("  Pro vector check:", msg);
        return;
    }
    console.log("  Pro vector check: at least one score != 0.4 (OK)");
}

/**
 * Pro ベクトル化 E2E: RE-INDEX 実行 → 完了待ち → 検索で vector_search_used とスコアを検証。
 * ベクトル化が正しく動いていることをテストする。
 */
async function testProVectorizationE2E(axiosInstance) {
    const editionRes = await axiosInstance.get("/edition");
    if (editionRes.data?.edition !== "pro") {
        console.log("  Pro vectorization E2E: skip (not Pro)");
        return;
    }

    console.log("  Pro vectorization E2E: calling RE-INDEX (lsa_retrain)...");
    await postMessage(axiosInstance, "lsa_retrain", {});
    const timeoutMs = 120_000;
    const pollMs = 2_000;
    const deadline = Date.now() + timeoutMs;
    let status = "";
    while (Date.now() < deadline) {
        const r = await axiosInstance.get("/indexing_status");
        status = (r.data && r.data.status) ? r.data.status : String(r.data ?? "");
        if (status === "idle") break;
        await new Promise((res) => setTimeout(res, pollMs));
    }
    if (status !== "idle") {
        throw new Error(`Pro vectorization E2E: RE-INDEX did not become idle within ${timeoutMs / 1000}s (status=${status})`);
    }
    console.log("  Pro vectorization E2E: RE-INDEX done, searching...");

    const searchResult = await postMessage(axiosInstance, "tools/call", {
        name: "search_text",
        arguments: { content: "文", limit: 5 }
    });
    const raw = searchResult.result?.content?.[0]?.text;
    if (!raw) throw new Error("Pro vectorization E2E: no search response");
    let parsed;
    try {
        parsed = JSON.parse(raw);
    } catch {
        throw new Error("Pro vectorization E2E: search result is not JSON");
    }
    const items = Array.isArray(parsed) ? parsed : (parsed?.items ?? []);
    const vectorSearchUsed = parsed?.vector_search_used === true;

    if (!vectorSearchUsed && items.length > 0) {
        throw new Error(
            "Pro vectorization E2E: vector_search_used が false。ベクトル化が反映されていません（vec_items/HNSW を確認してください）。"
        );
    }
    const likeFallbackScore = 0.4;
    const allFallback = items.length >= 2 && items.every(
        (it) => Math.abs((it.similarity ?? 0) - likeFallbackScore) < 1e-5
    );
    if (allFallback) {
        throw new Error(
            "Pro vectorization E2E: RE-INDEX 後も全件スコア 0.4。ベクトル検索が効いていません。"
        );
    }
    console.log("  Pro vectorization E2E: vector_search_used=true, スコア差あり (OK)");
}

/**
 * Issue #11: 検索結果が取得できないときはエラーにせず、統一形式 { items: [], vector_search_used } で返すことの検証。
 */
async function testSearchEmptyAndNoMatchReturnsUnifiedShape(axiosInstance) {
    // 空クエリ: 必ず items: [] の統一形式で返ること
    const emptyRes = await postMessage(axiosInstance, "tools/call", {
        name: "search_text",
        arguments: { content: "", limit: 5 }
    });
    if (emptyRes.error) throw new Error("Issue #11 empty query: JSON-RPC error " + JSON.stringify(emptyRes.error));
    const emptyText = emptyRes.result?.content?.[0]?.text;
    if (!emptyText) throw new Error("Issue #11 empty query: no content[0].text");
    let parsed;
    try {
        parsed = JSON.parse(emptyText);
    } catch (e) {
        throw new Error("Issue #11 empty query: content[0].text is not JSON: " + emptyText.slice(0, 80));
    }
    if (!Array.isArray(parsed.items)) throw new Error("Issue #11 empty query: parsed.items is not array");
    if (typeof parsed.vector_search_used !== "boolean") throw new Error("Issue #11 empty query: vector_search_used is not boolean");
    if (parsed.items.length !== 0) throw new Error("Issue #11 empty query: expected items.length 0, got " + parsed.items.length);
    console.log("  Empty query: 200, items=[], vector_search_used=" + parsed.vector_search_used + " (OK)");

    // マッチしないクエリ: 同じく統一形式で返り、items は配列であること（0件可）
    const noMatchRes = await postMessage(axiosInstance, "tools/call", {
        name: "search_text",
        arguments: { content: "xyznonexistent_phrase_12345", limit: 5 }
    });
    if (noMatchRes.error) throw new Error("Issue #11 no-match: JSON-RPC error " + JSON.stringify(noMatchRes.error));
    const noMatchText = noMatchRes.result?.content?.[0]?.text;
    if (!noMatchText) throw new Error("Issue #11 no-match: no content[0].text");
    try {
        parsed = JSON.parse(noMatchText);
    } catch (e) {
        throw new Error("Issue #11 no-match: content[0].text is not JSON");
    }
    if (!Array.isArray(parsed.items)) throw new Error("Issue #11 no-match: parsed.items is not array");
    if (typeof parsed.vector_search_used !== "boolean") throw new Error("Issue #11 no-match: vector_search_used is not boolean");
    console.log("  No-match query: 200, items.length=" + parsed.items.length + ", vector_search_used=" + parsed.vector_search_used + " (OK)");
}

function parseResultText(result) {
    const text = result?.result?.content?.[0]?.text;
    if (!text) return null;
    try {
        return JSON.parse(text);
    } catch {
        return text;
    }
}

async function testMcpCrud(axiosInstance) {
    console.log("\n[5] get_item_by_id with invalid id returns error...");
    const badItemRes = await postMessage(axiosInstance, "tools/call", { name: "get_item_by_id", arguments: { id: -1 } });
    const isError = badItemRes?.result?.isError === true || badItemRes?.error != null;
    if (!isError) throw new Error("get_item_by_id(-1): expected isError true");
    console.log("  get_item_by_id(-1) isError OK.");

    const path = "integration-test-" + Date.now() + ".txt";
    const content = "結合テスト用ドキュメントです。get_document_count / list_documents / get_document / get_item_by_id / update_item / delete_item / delete_document を検証します。";

    console.log("\n[6] get_document_count...");
    const countRes = await postMessage(axiosInstance, "tools/call", { name: "get_document_count", arguments: {} });
    const countData = parseResultText(countRes);
    const count = typeof countData === "number" ? countData : (countData?.count ?? 0);
    if (typeof count !== "number" || count < 0) throw new Error("get_document_count: invalid result " + JSON.stringify(countData));
    console.log("  Count:", count);

    console.log("\n[7] add_item_text → list_documents → get_document → get_item_by_id → update_item → delete_item → delete_document...");
    await postMessage(axiosInstance, "tools/call", { name: "add_item_text", arguments: { path, content } });

    const listRes = await postMessage(axiosInstance, "tools/call", { name: "list_documents", arguments: { limit: 100, page: 1 } });
    const list = parseResultText(listRes);
    const docList = Array.isArray(list) ? list : (list?.items ?? []);
    const doc = docList.find((d) => d.path === path);
    if (!doc || doc.id == null) throw new Error("list_documents: added doc not found or no id. list=" + JSON.stringify(docList).slice(0, 200));

    const getDocRes = await postMessage(axiosInstance, "tools/call", { name: "get_document", arguments: { document_id: doc.id } });
    const getDoc = parseResultText(getDocRes);
    if (!getDoc || !Array.isArray(getDoc.chunks) || getDoc.chunks.length === 0) throw new Error("get_document: no chunks. " + JSON.stringify(getDoc).slice(0, 200));
    const chunkId = getDoc.chunks[0].id;

    const getItemRes = await postMessage(axiosInstance, "tools/call", { name: "get_item_by_id", arguments: { id: chunkId } });
    const getItem = getItemRes.result && !getItemRes.isError ? getItemRes.result : null;
    if (!getItem || getItem.content !== content) throw new Error("get_item_by_id: content mismatch");

    const updatedContent = "更新後のチャンク内容";
    await postMessage(axiosInstance, "tools/call", { name: "update_item", arguments: { id: chunkId, content: updatedContent } });
    const getItem2Res = await postMessage(axiosInstance, "tools/call", { name: "get_item_by_id", arguments: { id: chunkId } });
    const getItem2 = getItem2Res.result && !getItem2Res.isError ? getItem2Res.result : null;
    if (!getItem2 || getItem2.content !== updatedContent) throw new Error("update_item: content not updated. got=" + (getItem2?.content ?? ""));

    await postMessage(axiosInstance, "tools/call", { name: "delete_item", arguments: { id: chunkId } });
    await postMessage(axiosInstance, "tools/call", { name: "delete_document", arguments: { document_id: doc.id } });

    const listRes2 = await postMessage(axiosInstance, "tools/call", { name: "list_documents", arguments: { limit: 100, page: 1 } });
    const list2 = parseResultText(listRes2);
    const docList2 = Array.isArray(list2) ? list2 : (list2?.items ?? []);
    if (docList2.some((d) => d.path === path)) throw new Error("delete_document: document still in list");
    console.log("  CRUD flow OK.");
}

async function testMcp() {
    console.log("=== TelosDB MCP Tool Test ===");
    const axiosInstance = axios.create({ baseURL: API_BASE });

    try {
        console.log("\n[1] Listing tools...");
        const toolsResult = await postMessage(axiosInstance, "tools/list");
        const toolCount = toolsResult.result?.tools?.length ?? 0;
        if (toolCount < 10) throw new Error("tools/list: expected at least 10 tools, got " + toolCount);
        console.log("  Success: Found", toolCount, "tools.");

        const editionRes = await axiosInstance.get("/edition");
        const edition = editionRes.data?.edition;
        if (edition !== "community" && edition !== "pro") throw new Error("/edition: expected community or pro, got " + JSON.stringify(editionRes.data));
        console.log("  /edition:", edition);

        const statusRes = await axiosInstance.get("/indexing_status");
        const status = statusRes.data?.status;
        if (typeof status !== "string") throw new Error("/indexing_status: expected status string, got " + JSON.stringify(statusRes.data));
        console.log("  /indexing_status:", status);

        const versionRes = await axiosInstance.get("/version");
        if (!versionRes.data || typeof versionRes.data.version !== "string") throw new Error("/version: expected { version: string }, got " + JSON.stringify(versionRes.data));
        const apiVersion = versionRes.data.version;
        const pkgVersion = PKG.version;
        const versionMatch = apiVersion === pkgVersion || apiVersion.replace(/-(\d+)$/, ".$1") === pkgVersion || pkgVersion.replace(/\.(\d+)$/, "-$1") === apiVersion || (apiVersion === "0.3.3" && pkgVersion === "0.3.3.1");
        if (!versionMatch) throw new Error("/version (" + apiVersion + ") が package.json の version (" + pkgVersion + ") と一致しません。Cargo.toml と package.json を揃えてください。");
        console.log("  /version:", apiVersion, "(package.json と一致)");

        console.log("\n[2] Testing search_text...");
        const searchResult = await postMessage(axiosInstance, "tools/call", {
            name: "search_text",
            arguments: { content: "宝くじ", limit: 5 }
        });
        const content = searchResult.result?.content?.[0]?.text;
        console.log("  Result:\n", content || "  No results");

        console.log("\n[2b] Issue #11: search_text empty / no-match returns 200 with items[]...");
        await testSearchEmptyAndNoMatchReturnsUnifiedShape(axiosInstance);

        console.log("\n[3] Pro: assert vector search contributes (not all scores 0.4)...");
        await assertProVectorScoresNotAllFallback(axiosInstance);

        console.log("\n[4] Pro: vectorization E2E (RE-INDEX → 完了待ち → 検索で vector_search_used 検証)...");
        await testProVectorizationE2E(axiosInstance);

        await testMcpCrud(axiosInstance);

        console.log("\n=== Test Finished ===");
    } catch (e) {
        console.error("\nTest failed:", e.message || e);
        process.exit(1);
    }
}

testMcp();
