import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { splitIntoParagraphs, aggregateBlocks, cosine, similarityCurve, valleyDepths, selectBoundaries, snapToHeadings, buildSections, chunkText } from "./semantic-chunker.js";
describe("splitIntoParagraphs", () => {
    it("splits on double newlines", () => {
        const text = "First paragraph.\n\nSecond paragraph.\n\nThird.";
        const result = splitIntoParagraphs(text);
        assert.deepStrictEqual(result, [
            "First paragraph.",
            "Second paragraph.",
            "Third.",
        ]);
    });
    it("falls back to sentence splitting for wall of text", () => {
        const wall = "First sentence. Second sentence. Third sentence here.";
        const result = splitIntoParagraphs(wall);
        assert.ok(result.length >= 2, `Expected >=2 paragraphs, got ${result.length}`);
    });
    it("filters empty paragraphs", () => {
        const text = "A.\n\n\n\nB.\n\n\n\nC.";
        const result = splitIntoParagraphs(text);
        assert.deepStrictEqual(result, ["A.", "B.", "C."]);
    });
});
describe("aggregateBlocks", () => {
    it("merges short paragraphs until minimum size", () => {
        const paragraphs = ["Hi.", "Hello.", "Short.", "This is a much longer paragraph that should definitely exceed the minimum block size on its own without needing any merging at all because it has enough content."];
        const blocks = aggregateBlocks(paragraphs, 100);
        assert.ok(blocks.length >= 1);
        assert.ok(blocks.length <= paragraphs.length);
        for (const b of blocks) {
            if (b !== blocks[blocks.length - 1]) {
                assert.ok(b.text.length >= 100, `Block too small: ${b.text.length} bytes`);
            }
        }
    });
    it("preserves paragraph list in each block", () => {
        const paragraphs = ["Short A.", "Short B.", "Long paragraph that exceeds minimum size on its own easily."];
        const blocks = aggregateBlocks(paragraphs, 50);
        const allParagraphs = blocks.flatMap((b) => b.paragraphs);
        assert.deepStrictEqual(allParagraphs, paragraphs);
    });
    it("detects headings in blocks", () => {
        const paragraphs = ["## Section Title\n\nSome content here.", "More content."];
        const blocks = aggregateBlocks(paragraphs, 10);
        const headingBlock = blocks.find((b) => b.headingLine !== null);
        assert.ok(headingBlock, "Expected a block with a heading");
        assert.strictEqual(headingBlock.headingLine, "## Section Title");
    });
});
describe("cosine", () => {
    it("returns 1.0 for identical vectors", () => {
        const v = [1, 2, 3];
        assert.ok(Math.abs(cosine(v, v) - 1.0) < 0.001);
    });
    it("returns 0.0 for orthogonal vectors", () => {
        const a = [1, 0, 0];
        const b = [0, 1, 0];
        assert.ok(Math.abs(cosine(a, b)) < 0.001);
    });
    it("returns -1.0 for opposite vectors", () => {
        const a = [1, 0];
        const b = [-1, 0];
        assert.ok(Math.abs(cosine(a, b) - (-1.0)) < 0.001);
    });
});
describe("similarityCurve", () => {
    it("computes cosine between consecutive embeddings", () => {
        const embeddings = [[1, 0], [1, 0], [1, 0]];
        const curve = similarityCurve(embeddings);
        assert.strictEqual(curve.length, 2);
        assert.ok(Math.abs(curve[0] - 1.0) < 0.001);
        assert.ok(Math.abs(curve[1] - 1.0) < 0.001);
    });
    it("detects low similarity between different vectors", () => {
        const embeddings = [[1, 0], [0, 1], [1, 0]];
        const curve = similarityCurve(embeddings);
        assert.strictEqual(curve.length, 2);
        assert.ok(curve[0] < 0.1);
        assert.ok(curve[1] < 0.1);
    });
});
describe("valleyDepths", () => {
    it("finds deep valley between peaks", () => {
        const curve = [0.9, 0.8, 0.2, 0.85, 0.9];
        const depths = valleyDepths(curve);
        assert.strictEqual(depths.length, curve.length);
        const maxIdx = depths.indexOf(Math.max(...depths));
        assert.strictEqual(maxIdx, 2);
    });
    it("returns zeros for flat curve", () => {
        const curve = [0.8, 0.8, 0.8, 0.8];
        const depths = valleyDepths(curve);
        for (const d of depths) {
            assert.ok(Math.abs(d) < 0.001);
        }
    });
});
describe("selectBoundaries", () => {
    it("selects top N deepest valleys sorted by position", () => {
        const depths = [0, 0.5, 0.1, 0.8, 0];
        const boundaries = selectBoundaries(depths, 2);
        assert.deepStrictEqual(boundaries, [1, 3]);
    });
    it("never selects more than available positions", () => {
        const depths = [0.5, 0.3];
        const boundaries = selectBoundaries(depths, 10);
        assert.ok(boundaries.length <= 2);
    });
    it("skips zero-depth positions", () => {
        const depths = [0, 0, 0];
        const boundaries = selectBoundaries(depths, 5);
        assert.strictEqual(boundaries.length, 0);
    });
});
describe("snapToHeadings", () => {
    it("snaps boundary to nearby heading", () => {
        const blocks = [
            { text: "Intro text", paragraphs: ["Intro text"], headingLine: null },
            { text: "More intro", paragraphs: ["More intro"], headingLine: null },
            { text: "## New Topic\nContent", paragraphs: ["## New Topic\nContent"], headingLine: "## New Topic" },
            { text: "Topic content", paragraphs: ["Topic content"], headingLine: null },
        ];
        const snapped = snapToHeadings([1], blocks, 2);
        assert.deepStrictEqual(snapped, [2]);
    });
    it("converts curve index to block index when no heading nearby", () => {
        const blocks = [
            { text: "A", paragraphs: ["A"], headingLine: null },
            { text: "B", paragraphs: ["B"], headingLine: null },
            { text: "C", paragraphs: ["C"], headingLine: null },
        ];
        // Curve index 1 = gap between block 1 and 2 → new section starts at block 2
        const snapped = snapToHeadings([1], blocks, 2);
        assert.deepStrictEqual(snapped, [2]);
    });
});
describe("buildSections", () => {
    it("creates sections from blocks and boundaries", () => {
        const blocks = [
            { text: "First para. Second sentence here.", paragraphs: ["First para. Second sentence here."], headingLine: null },
            { text: "## Topic B\nContent of B.", paragraphs: ["## Topic B\nContent of B."], headingLine: "## Topic B" },
            { text: "More B content here.", paragraphs: ["More B content here."], headingLine: null },
        ];
        const sections = buildSections(blocks, [1]);
        assert.strictEqual(sections.length, 2);
        assert.strictEqual(sections[0].title, "First para.");
        assert.strictEqual(sections[1].title, "Topic B");
        assert.ok(sections[0].chunks.length >= 1);
        assert.ok(sections[1].chunks.length >= 1);
    });
    it("assigns sequential chunk IDs across sections", () => {
        const blocks = [
            { text: "A content.", paragraphs: ["A content."], headingLine: null },
            { text: "B content.", paragraphs: ["B content."], headingLine: null },
        ];
        const sections = buildSections(blocks, [1]);
        const allIds = sections.flatMap((s) => s.chunks.map((c) => c.id));
        const expected = allIds.map((_, i) => i);
        assert.deepStrictEqual(allIds, expected);
    });
});
// Mock embedder: deterministic vectors from text hash
function mockEmbed(texts) {
    return Promise.resolve(texts.map((t) => {
        let hash = 0;
        for (let i = 0; i < t.length; i++)
            hash = ((hash << 5) - hash + t.charCodeAt(i)) | 0;
        return [
            Math.sin(hash),
            Math.cos(hash),
            Math.sin(hash * 2),
            Math.cos(hash * 3),
        ];
    }));
}
describe("chunkText", () => {
    it("returns single section for very short text", async () => {
        const sections = await chunkText("Short text.", mockEmbed);
        assert.strictEqual(sections.length, 1);
        assert.strictEqual(sections[0].chunks.length, 1);
    });
    it("produces multiple sections for multi-topic document", async () => {
        const topic1 = Array(10).fill("The financial quarterly earnings report shows strong revenue growth in the technology sector with cloud computing services leading the expansion.").join("\n\n");
        const topic2 = Array(10).fill("The recipe for chocolate cake requires flour sugar eggs butter cocoa powder baking soda vanilla extract and a pinch of salt mixed together carefully.").join("\n\n");
        const text = topic1 + "\n\n" + topic2;
        const sections = await chunkText(text, mockEmbed);
        assert.ok(sections.length >= 2, `Expected >=2 sections, got ${sections.length}`);
    });
    it("respects MAX_SECTIONS ceiling", async () => {
        const bigText = Array(200).fill("Paragraph content that is different each time number " + Math.random() + " to ensure variety.").join("\n\n");
        const sections = await chunkText(bigText, mockEmbed);
        assert.ok(sections.length <= 25, `Expected <=25 sections, got ${sections.length}`);
    });
    it("handles markdown with headings as tiebreakers", async () => {
        const text = [
            "# Introduction",
            "Welcome to the document. This covers many topics in great detail.",
            "",
            "Some introductory material about the background and context of this work.",
            "",
            "# Methods",
            "We used a comprehensive approach to analyze the data collected from various sources.",
            "",
            "Statistical methods included regression analysis and hypothesis testing.",
        ].join("\n");
        const sections = await chunkText(text, mockEmbed);
        assert.ok(sections.length >= 1);
        const titles = sections.map((s) => s.title);
        const hasIntro = titles.some((t) => t.includes("Introduction"));
        const hasMethods = titles.some((t) => t.includes("Methods"));
        assert.ok(hasIntro || hasMethods, `Expected heading-derived titles, got: ${titles.join(", ")}`);
    });
    it("all chunks have sequential IDs starting from 0", async () => {
        const text = Array(20).fill("Some paragraph content that is moderately long to ensure multiple blocks.").join("\n\n");
        const sections = await chunkText(text, mockEmbed);
        const allIds = sections.flatMap((s) => s.chunks.map((c) => c.id));
        for (let i = 0; i < allIds.length; i++) {
            assert.strictEqual(allIds[i], i, `Expected chunk ID ${i}, got ${allIds[i]}`);
        }
    });
    it("every chunk has a preview", async () => {
        const text = Array(15).fill("Content paragraph with enough text to be meaningful and create blocks.").join("\n\n");
        const sections = await chunkText(text, mockEmbed);
        for (const section of sections) {
            for (const chunk of section.chunks) {
                assert.ok(chunk.preview.length > 0, "Chunk preview must not be empty");
                assert.ok(chunk.preview.length <= 150, "Chunk preview must not exceed 150 chars");
            }
        }
    });
});
//# sourceMappingURL=semantic-chunker.test.js.map