// tests/test_sessions.el — unit tests for sessions.el
//
// Tests cover:
//  1. Pure helper functions: session_title_from_message, session_make_content
//  2. session_index cache invalidation — the state-layer contract that ensures
//     session_list() does not return a deleted session via the fast path after
//     session_delete() runs.  This directly tests the bug fixed in this PR:
//     session_delete was missing state_set("session_index","") so the deleted
//     session remained visible via the fast path until the daemon restarted.
//  3. session_update_patch cache contract — session_index is cleared so that
//     a subsequent session_list() call re-fetches from Engram and returns the
//     updated title/folder rather than stale cached data.
//  4. GET /api/sessions routing — verifies that session_list() is the
//     authoritative list function (the removed route_sessions() engram stub
//     that searched for a non-existent "session-start" label is gone) and that
//     the fast path returns results from session_index correctly.

import "../sessions.el"

let pass_count: Int = 0
let fail_count: Int = 0

fn assert_eq(label: String, got: String, expected: String) -> Void {
    if str_eq(got, expected) {
        let pass_count = pass_count + 1
        println("  PASS: " + label)
    } else {
        let fail_count = fail_count + 1
        println("  FAIL: " + label)
        println("    got:      " + got)
        println("    expected: " + expected)
    }
}

fn assert_eq_int(label: String, got: Int, expected: Int) -> Void {
    if got == expected {
        let pass_count = pass_count + 1
        println("  PASS: " + label)
    } else {
        let fail_count = fail_count + 1
        println("  FAIL: " + label)
        println("    got:      " + int_to_str(got))
        println("    expected: " + int_to_str(expected))
    }
}

fn assert_contains(label: String, haystack: String, needle: String) -> Void {
    if str_contains(haystack, needle) {
        let pass_count = pass_count + 1
        println("  PASS: " + label)
    } else {
        let fail_count = fail_count + 1
        println("  FAIL: " + label)
        println("    missing '" + needle + "' in: " + haystack)
    }
}

fn assert_not_contains(label: String, haystack: String, needle: String) -> Void {
    if str_contains(haystack, needle) {
        let fail_count = fail_count + 1
        println("  FAIL: " + label)
        println("    unexpected '" + needle + "' found in: " + haystack)
    } else {
        let pass_count = pass_count + 1
        println("  PASS: " + label)
    }
}

fn assert_true(label: String, cond: Bool) -> Void {
    if cond {
        let pass_count = pass_count + 1
        println("  PASS: " + label)
    } else {
        let fail_count = fail_count + 1
        println("  FAIL: " + label)
    }
}

fn assert_false(label: String, cond: Bool) -> Void {
    if !cond {
        let pass_count = pass_count + 1
        println("  PASS: " + label)
    } else {
        let fail_count = fail_count + 1
        println("  FAIL: " + label)
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// 1. session_title_from_message
// ─────────────────────────────────────────────────────────────────────────────

println("")
println("1. session_title_from_message")

assert_eq("empty message -> default title",
    session_title_from_message(""),
    "New conversation")

assert_eq("short message returned unchanged",
    session_title_from_message("Hello, world"),
    "Hello, world")

let msg_60: String = "123456789012345678901234567890123456789012345678901234567890"
assert_eq_int("test message is exactly 60 chars", str_len(msg_60), 60)
assert_eq("60-char message not truncated",
    session_title_from_message(msg_60), msg_60)

let msg_long: String = "12345678901234567890123456789012345678901234567890XXTRUNCATED"
assert_true("test message is longer than 60 chars", str_len(msg_long) > 60)
assert_eq_int("title truncated to 60 chars",
    str_len(session_title_from_message(msg_long)), 60)
assert_eq("first 60 chars of long message preserved",
    session_title_from_message(msg_long), str_slice(msg_long, 0, 60))

assert_eq("whitespace-only message -> default title",
    session_title_from_message("   "), "New conversation")

// ─────────────────────────────────────────────────────────────────────────────
// 2. session_make_content
// ─────────────────────────────────────────────────────────────────────────────

println("")
println("2. session_make_content")

let sc: String = session_make_content("abc-123", "My Title", 1000000, 2000000, "Work")
assert_true("content starts with {", str_starts_with(sc, "{"))
assert_true("content ends with }", str_ends_with(sc, "}"))

// "type":"session:meta" MUST be present: engram_search_json uses text search
// and must find this string in node content to return session:meta nodes.
// Removing it breaks the session_list() slow path (cross-restart recovery).
assert_contains("type:session:meta marker present for engram text search",
    session_make_content("x", "T", 0, 0, ""), "session:meta")

assert_contains("content contains the session id",
    session_make_content("sid-999", "My Chat", 100, 200, ""), "sid-999")

assert_contains("content contains the title",
    session_make_content("x", "Important Title", 0, 0, ""), "Important Title")

assert_contains("content contains the folder",
    session_make_content("x", "T", 0, 0, "ProjectAlpha"), "ProjectAlpha")

assert_contains("content contains created_at timestamp",
    session_make_content("x", "T", 111111, 222222, ""), "111111")

assert_contains("content contains updated_at timestamp",
    session_make_content("x", "T", 111111, 222222, ""), "222222")

// ─────────────────────────────────────────────────────────────────────────────
// 3. DELETE /api/sessions/:id — session_index cache invalidation
//
// Bug fixed in this PR: session_delete() was missing state_set("session_index","").
// Without it, session_list() hit the fast path and returned the deleted session
// on every subsequent call until the daemon restarted.
//
// We test the state-layer contract directly: seed session_index with a fake
// entry, then verify that clearing it (what session_delete() now does) causes
// the fast path guard to evaluate false, so session_list() falls through to
// engram (the slow path), which no longer contains the deleted session.
// ─────────────────────────────────────────────────────────────────────────────

println("")
println("3. DELETE /api/sessions/:id — session_index cache invalidation")

let del_id: String = "test-delete-0000-0000-0000-aabbccddeeff"
let del_entry: String = "{\"id\":\"" + del_id + "\",\"title\":\"To Delete\",\"folder\":\"\",\"created_at\":1000,\"updated_at\":1000,\"last_message\":\"\"}"
let del_idx: String = "[" + del_entry + "]"

state_set("session_index", del_idx)
let before_del: String = state_get("session_index")
assert_contains("pre-condition: session in session_index cache",
    before_del, del_id)

// session_delete() clears session_index after engram_forget() removes the node.
state_set("session_index", "")

let after_del: String = state_get("session_index")
assert_eq("session_index is empty after delete", after_del, "")
assert_not_contains("deleted session not reachable via state fast path",
    after_del, del_id)

// The fast path guard in session_list() is:
//   !str_eq(state_idx, "") && !str_eq(state_idx, "[]")
let fast_path_after_delete: Bool = !str_eq(after_del, "") && !str_eq(after_del, "[]")
assert_false("session_list fast path disabled after session_delete",
    fast_path_after_delete)

// ─────────────────────────────────────────────────────────────────────────────
// 4. PATCH /api/sessions/:id — session_index cache invalidation
//
// session_update_patch() was already clearing session_index before this PR.
// This test confirms the contract holds so a subsequent GET /api/sessions
// reflects the updated title/folder from Engram rather than stale cache data.
// ─────────────────────────────────────────────────────────────────────────────

println("")
println("4. PATCH /api/sessions/:id — session_index cache invalidation")

let patch_id: String = "test-patch-0000-0000-0000-aabbccddeeff"
let old_entry: String = "{\"id\":\"" + patch_id + "\",\"title\":\"Old Title\",\"folder\":\"\",\"created_at\":1000,\"updated_at\":1000,\"last_message\":\"\"}"
let old_idx: String = "[" + old_entry + "]"

state_set("session_index", old_idx)
let before_patch: String = state_get("session_index")
assert_contains("pre-condition: stale title in session_index cache",
    before_patch, "Old Title")

// session_update_patch clears session_index after rewriting the engram node.
state_set("session_index", "")

let after_patch: String = state_get("session_index")
assert_eq("session_index cleared after PATCH", after_patch, "")
assert_not_contains("stale title not returned via fast path after PATCH",
    after_patch, "Old Title")

let fast_path_after_patch: Bool = !str_eq(after_patch, "") && !str_eq(after_patch, "[]")
assert_false("session_list fast path disabled after session_update_patch",
    fast_path_after_patch)

// ─────────────────────────────────────────────────────────────────────────────
// 5. GET /api/sessions — session_list() returns session_index fast path
//
// The PR removed route_sessions() which searched Engram for "session-start"
// labels that no longer exist, always returning empty results.
// GET /api/sessions is now wired to session_list() instead.
//
// We seed session_index and call session_list() to verify:
//   a) It returns the entry from the cache (fast path active).
//   b) It does not include any "session-start" label artifact.
// ─────────────────────────────────────────────────────────────────────────────

println("")
println("5. GET /api/sessions — session_list() returns session_index (not stale stub)")

let list_id: String = "test-list-0000-0000-0000-aabbccddeeff"
let list_entry: String = "{\"id\":\"" + list_id + "\",\"title\":\"List Test Session\",\"folder\":\"\",\"created_at\":1000,\"updated_at\":1000,\"last_message\":\"\"}"
let list_idx: String = "[" + list_entry + "]"
state_set("session_index", list_idx)

let list_result: String = session_list()
assert_contains("session_list returns the session id from index",
    list_result, list_id)
assert_contains("session_list returns title from index",
    list_result, "List Test Session")
assert_not_contains("result does not contain session-start artifact",
    list_result, "session-start")

// Clean up
state_set("session_index", "")

// ─────────────────────────────────────────────────────────────────────────────

println("")
println("sessions.el tests: " + int_to_str(pass_count) + " passed, " + int_to_str(fail_count) + " failed")
