import fs from "fs";
import path from "path";
import request from "supertest";
import { describe, expect, it } from "vitest";
import { ASSISTANT_SESSIONS_DIR } from "../src/config";
import { createApp } from "../src/server";

describe("assistant mode API", () => {
  it("processes message and returns assistant response with debug payload", async () => {
    const app = createApp();
    const response = await request(app).post("/api/assistant/message").send({
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      user_message: "Prover schet 97 i podsveti riskovye zony."
    });

    expect(response.status).toBe(200);
    expect(response.body.ok).toBe(true);
    expect(typeof response.body.session_id).toBe("string");
    expect(typeof response.body.assistant_reply).toBe("string");
    expect(typeof response.body.reply_type).toBe("string");
    expect(response.body.conversation_item?.role).toBe("assistant");
    expect(response.body.conversation_item?.reply_type).toBe(response.body.reply_type);
    expect(response.body.debug?.trace_id).toBeTypeOf("string");
    expect(Array.isArray(response.body.debug?.routes)).toBe(true);
    expect(Array.isArray(response.body.debug?.requirements_extracted)).toBe(true);
    expect(typeof response.body.debug?.coverage_report?.requirements_total).toBe("number");
    expect(typeof response.body.debug?.answer_grounding_check?.status).toBe("string");
    expect(Array.isArray(response.body.debug?.retrieval_status)).toBe(true);
    expect(Array.isArray(response.body.debug?.retrieval_results)).toBe(true);
    expect(Array.isArray(response.body.conversation)).toBe(true);
    expect(response.body.conversation.length).toBe(2);
  });

  it("keeps session-scoped history and returns it via session endpoint", async () => {
    const app = createApp();
    const first = await request(app).post("/api/assistant/message").send({
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      user_message: "Sdelai proverku po postavshchikam."
    });
    expect(first.status).toBe(200);
    const sessionId = String(first.body.session_id);

    const second = await request(app).post("/api/assistant/message").send({
      session_id: sessionId,
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      user_message: "Dobav proverku po periodu 2020-06."
    });
    expect(second.status).toBe(200);
    expect(second.body.session_id).toBe(sessionId);

    const session = await request(app).get(`/api/assistant/session/${sessionId}`);
    expect(session.status).toBe(200);
    expect(session.body.ok).toBe(true);
    expect(session.body.session?.session_id).toBe(sessionId);
    expect(Array.isArray(session.body.session?.items)).toBe(true);
    expect(session.body.session.items.length).toBe(4);
  });

  it("executes factual retrieval for routed fragments", async () => {
    const app = createApp();

    const riskResponse = await request(app).post("/api/assistant/message").send({
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      user_message: "Проверь НДС и рискованные записи по документам."
    });

    expect(riskResponse.status).toBe(200);
    expect(Array.isArray(riskResponse.body.debug?.retrieval_results)).toBe(true);
    expect(riskResponse.body.debug.retrieval_results.length).toBeGreaterThan(0);
    expect(riskResponse.body.debug.retrieval_results.some((item: { route?: string }) => item.route === "store_feature_risk")).toBe(true);
    expect(riskResponse.body.debug.retrieval_results.some((item: { status?: string }) => item.status === "ok")).toBe(true);
    expect(typeof riskResponse.body.reply_type).toBe("string");
    expect(["factual_with_explanation", "partial_coverage"]).toContain(riskResponse.body.reply_type);
    expect(String(riskResponse.body.assistant_reply)).toContain("Почему это попало в ответ");

    const chainResponse = await request(app).post("/api/assistant/message").send({
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      user_message: "Разложи цепочку документов и оплат по контрагентам."
    });

    expect(chainResponse.status).toBe(200);
    expect(Array.isArray(chainResponse.body.debug?.retrieval_results)).toBe(true);
    expect(chainResponse.body.debug.retrieval_results.some((item: { route?: string }) => item.route === "hybrid_store_plus_live")).toBe(true);
    const answerStructure = chainResponse.body.debug?.answer_structure_v11;
    const evidenceBlock = answerStructure?.evidence_block;
    if (Array.isArray(evidenceBlock?.evidence_ids) && evidenceBlock.evidence_ids.length > 0) {
      expect(Array.isArray(evidenceBlock.claim_evidence_links)).toBe(true);
      expect(typeof evidenceBlock.claim_evidence_links[0]?.claim_ref).toBe("string");
      expect(Array.isArray(evidenceBlock.claim_evidence_links[0]?.evidence_ids)).toBe(true);
    }
    expect(String(chainResponse.body.assistant_reply)).toContain("Основание отбора");
  });

  it("keeps in-domain translit queries in scope and routed", async () => {
    const app = createApp();

    const response = await request(app).post("/api/assistant/message").send({
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      user_message: "Prover schet 60 za 2020-06, gde taili postavshikov i kakie dokumenty ne zakryvayut oplaty."
    });

    expect(response.status).toBe(200);
    expect(response.body.reply_type).not.toBe("out_of_scope");
    expect(response.body.debug?.route_summary?.message_in_scope).toBe(true);
    expect(Array.isArray(response.body.debug?.routes)).toBe(true);
    expect(response.body.debug?.routes.some((item: { route?: string }) => item.route !== "no_route")).toBe(true);
    expect(Array.isArray(response.body.debug?.retrieval_results)).toBe(true);
    expect(response.body.debug?.retrieval_results.some((item: { status?: string }) => item.status === "ok")).toBe(true);
  });

  it("avoids false route mismatch when supported evidence exists for bounded answer", async () => {
    const app = createApp();

    const response = await request(app).post("/api/assistant/message").send({
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      user_message:
        "Покажи хвосты поставщиков по счету 60 за 2020-06 и выдели, где проблема уже похожа на разрыв цепочки документов."
    });

    expect(response.status).toBe(200);
    expect(response.body.reply_type).not.toBe("route_mismatch_blocked");
    expect(response.body.debug?.answer_grounding_check?.status).not.toBe("route_mismatch_blocked");
    expect(["partial", "grounded"]).toContain(String(response.body.debug?.answer_grounding_check?.status));
    expect(response.body.reply_type).toBe("partial_coverage");
  });

  it("blocks answer when critical domain token is not grounded", async () => {
    const app = createApp();

    const response = await request(app).post("/api/assistant/message").send({
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      user_message: "Проверь основные средства и рискованные записи."
    });

    expect(response.status).toBe(200);
    expect(response.body.reply_type).toBe("route_mismatch_blocked");
    expect(response.body.debug?.answer_grounding_check?.status).toBe("route_mismatch_blocked");
    expect(response.body.debug?.answer_grounding_check?.route_subject_match).toBe(false);
    expect(Array.isArray(response.body.debug?.answer_grounding_check?.reasons)).toBe(true);
    expect(String(response.body.assistant_reply)).toContain("предмет результата не совпал");
  });

  it("applies semantic narrowing profile for hybrid retrieval without GUID", async () => {
    const app = createApp();

    const first = await request(app).post("/api/assistant/message").send({
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      user_message: "Разложи цепочку по 51 и 60 счетам: где закрытие не тем документом."
    });
    expect(first.status).toBe(200);

    const second = await request(app).post("/api/assistant/message").send({
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      user_message: "Разложи цепочку по банку: где выписка, документ и проводка живут отдельно и повторяется паттерн."
    });
    expect(second.status).toBe(200);

    const firstHybrid = (first.body.debug?.retrieval_results ?? []).find((item: { route?: string }) => item.route === "hybrid_store_plus_live");
    const secondHybrid = (second.body.debug?.retrieval_results ?? []).find((item: { route?: string }) => item.route === "hybrid_store_plus_live");

    expect(firstHybrid).toBeTruthy();
    expect(secondHybrid).toBeTruthy();

    const firstSummary = (firstHybrid as { summary?: Record<string, unknown> }).summary ?? {};
    const secondSummary = (secondHybrid as { summary?: Record<string, unknown> }).summary ?? {};

    expect(firstSummary.semantic_narrowing_applied).toBe(true);
    expect(typeof firstSummary.source_records).toBe("number");
    expect(typeof firstSummary.filtered_records_after_narrowing).toBe("number");
    expect(Number(firstSummary.filtered_records_after_narrowing)).toBeLessThan(Number(firstSummary.source_records));

    const firstProfile = firstSummary.semantic_profile as Record<string, unknown>;
    const secondProfile = secondSummary.semantic_profile as Record<string, unknown>;
    expect(firstProfile).toBeTruthy();
    expect(secondProfile).toBeTruthy();

    expect(Array.isArray(firstProfile.account_scope)).toBe(true);
    expect((firstProfile.account_scope as string[]).includes("51")).toBe(true);
    expect((firstProfile.account_scope as string[]).includes("60")).toBe(true);

    expect(Array.isArray(firstProfile.anomaly_patterns)).toBe(true);
    expect(Array.isArray(secondProfile.anomaly_patterns)).toBe(true);
    expect((firstProfile.anomaly_patterns as string[]).includes("wrong_document_type")).toBe(true);
    expect((secondProfile.anomaly_patterns as string[]).includes("repeated_anomaly")).toBe(true);
  });

  it("writes one persistent JSON log file per session", async () => {
    const app = createApp();
    const sessionId = `asst-test-${Date.now()}-${Math.floor(Math.random() * 10000)}`;

    const first = await request(app).post("/api/assistant/message").send({
      session_id: sessionId,
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      user_message: "Проверь НДС."
    });
    expect(first.status).toBe(200);

    const second = await request(app).post("/api/assistant/message").send({
      session_id: sessionId,
      useMock: true,
      promptVersion: "normalizer_v2_0_2",
      message: "Разложи цепочку документов по контрагентам."
    });
    expect(second.status).toBe(200);

    const logPath = path.resolve(ASSISTANT_SESSIONS_DIR, `${sessionId}.json`);
    expect(fs.existsSync(logPath)).toBe(true);

    const logPayload = JSON.parse(fs.readFileSync(logPath, "utf-8")) as {
      schema_version: string;
      session_id: string;
      counters: {
        total_messages: number;
        user_messages: number;
        assistant_messages: number;
      };
      turns: Array<{
        human_block: string;
        human_readable: {
          question_raw: string;
          question_understood: string;
          decomposition: string[];
          answer: string;
        };
      }>;
      conversation: unknown[];
    };

    expect(logPayload.schema_version).toBe("assistant_session_log_v1");
    expect(logPayload.session_id).toBe(sessionId);
    expect(logPayload.counters.total_messages).toBe(4);
    expect(logPayload.counters.user_messages).toBe(2);
    expect(logPayload.counters.assistant_messages).toBe(2);
    expect(Array.isArray(logPayload.turns)).toBe(true);
    expect(logPayload.turns.length).toBe(2);
    expect(logPayload.turns[0].human_block).toContain("Вопрос:");
    expect(logPayload.turns[0].human_block).toContain("Понято как:");
    expect(logPayload.turns[0].human_block).toContain("Декомпозиция:");
    expect(logPayload.turns[0].human_block).toContain("Ответ:");
    expect(Array.isArray(logPayload.turns[0].human_readable.decomposition)).toBe(true);
    expect(Array.isArray(logPayload.conversation)).toBe(true);
    expect(logPayload.conversation.length).toBe(4);

    const sameSessionFiles = fs.readdirSync(ASSISTANT_SESSIONS_DIR).filter((item) => item === `${sessionId}.json`);
    expect(sameSessionFiles.length).toBe(1);

    fs.unlinkSync(logPath);
  });
});
