use crate::memory::lesson::{LessonMemory, LessonMetadata};
use crate::workstream::{WorkStream, WorkStreamStatus};

use super::super::host::HostKind;
use super::super::injection_gate::{ContextGateAction, ContextGateDecision};
use super::super::invocation::ContextInvocation;
use super::super::policy::ContextLimits;
use super::super::render::{
    append_context_gate_debug_trace, build_context_header, build_context_stats_footer,
    empty_context_output, generate_context_for_test, render_context_output, ContextRenderStats,
    SectionRenderStats,
};
use super::super::sections::{
    render_core_memory, render_core_memory_with_limits, render_lessons_with_limit,
    render_memory_index, render_memory_index_with_limits,
    render_memory_index_with_limits_excluding, render_recent_sessions,
    render_recent_sessions_with_limit, render_workstreams, render_workstreams_with_limits,
};
use super::super::types::{ContextRequest, SessionSummaryBrief};
use super::{sample_memory, sample_memory_with_epoch, sample_workstream};

#[test]
fn render_recent_sessions_truncates_completed_line() {
    let mut output = String::new();
    let summaries = vec![SessionSummaryBrief {
        request: "Implement feature".to_string(),
        completed: Some(format!("{}\nignored", "x".repeat(130))),
        created_at_epoch: 1_710_000_000,
    }];

    render_recent_sessions(&mut output, &summaries);

    assert!(output.contains("Implement feature"));
    assert!(output.contains("=> "));
    assert!(output.contains("..."));
    assert!(!output.contains("ignored"));
}

#[test]
fn post_gate_debug_trace_preserves_request_source_for_delta_output() {
    let request = ContextRequest {
        cwd: "/tmp/remem".to_string(),
        project: "/tmp/remem".to_string(),
        session_id: Some("sess-1".to_string()),
        hook_source: Some("compact".to_string()),
        current_branch: Some("main".to_string()),
        host: HostKind::CodexCli,
        use_colors: false,
    };
    let decision = ContextGateDecision {
        output: String::new(),
        action: ContextGateAction::EmittedDelta,
        reason: "changed_hash",
        key: Some("session:/tmp/remem:sess-1".to_string()),
        context_hash: Some("hash-a".to_string()),
        output_mode: Some("delta"),
    };
    let mut output = "[remem context delta truncated]\n".to_string();

    append_context_gate_debug_trace(&mut output, &request, &decision);

    assert!(output.contains(
        "- request host=codex-cli project=/tmp/remem cwd=/tmp/remem branch=main session=sess-1 source=compact"
    ));
    assert!(output.contains("- gate action=EmittedDelta reason=changed_hash output_mode=delta"));
}

#[test]
fn render_recent_sessions_truncates_request_text() {
    let mut output = String::new();
    let long_request = format!("Investigate SessionStart budget {}", "x".repeat(300));
    let summaries = vec![SessionSummaryBrief {
        request: long_request.clone(),
        completed: Some("done".to_string()),
        created_at_epoch: 1_710_000_000,
    }];

    render_recent_sessions(&mut output, &summaries);

    assert!(output.contains("Investigate SessionStart budget"));
    assert!(output.contains("..."));
    assert!(!output.contains(&long_request));
}

#[test]
fn render_recent_sessions_respects_char_limit() {
    let mut output = String::new();
    let summaries = vec![
        SessionSummaryBrief {
            request: "Short followup".to_string(),
            completed: Some("done".to_string()),
            created_at_epoch: 1_710_000_000,
        },
        SessionSummaryBrief {
            request: "Second session should not fit".to_string(),
            completed: Some("done".to_string()),
            created_at_epoch: 1_710_000_100,
        },
    ];

    render_recent_sessions_with_limit(&mut output, &summaries, 70);

    assert!(output.contains("Short followup"));
    assert!(!output.contains("Second session should not fit"));
    assert!(output.chars().count() <= 70);
}

#[test]
fn render_memory_index_prioritizes_known_types() {
    let mut output = String::new();
    let memories = vec![
        sample_memory(1, "custom", "Custom title"),
        sample_memory(2, "bugfix", "Fix title"),
        sample_memory(3, "decision", "Decision title"),
    ];

    render_memory_index(&mut output, &memories);

    let decision_pos = output.find("**Decisions**").unwrap();
    let bugfix_pos = output.find("**Bug Fixes**").unwrap();
    let custom_pos = output.find("**custom**").unwrap();
    assert!(decision_pos < bugfix_pos);
    assert!(bugfix_pos < custom_pos);
}

#[test]
fn render_memory_index_labels_and_orders_procedure_memories() {
    let mut output = String::new();
    let memories = vec![
        sample_memory(1, "session_activity", "Recent session"),
        sample_memory(2, "procedure", "Review procedure"),
        sample_memory(3, "discovery", "Discovery title"),
    ];

    render_memory_index(&mut output, &memories);

    let discovery_pos = output.find("**Discoveries**").unwrap();
    let procedure_pos = output.find("**Procedures**").unwrap();
    let session_pos = output.find("**Sessions**").unwrap();
    assert!(output.contains("Review procedure"));
    assert!(discovery_pos < procedure_pos);
    assert!(procedure_pos < session_pos);
}

#[test]
fn render_memory_index_excludes_preferences() {
    let mut output = String::new();
    let memories = vec![
        sample_memory(1, "preference", "Preference title"),
        sample_memory(2, "decision", "Decision title"),
    ];

    render_memory_index(&mut output, &memories);

    assert!(output.contains("Decision title"));
    assert!(!output.contains("Preference title"));
    assert!(!output.contains("**Preferences**"));
}

#[test]
fn render_memory_index_excludes_lessons() {
    let mut output = String::new();
    let memories = vec![
        sample_memory(1, "lesson", "Lesson title"),
        sample_memory(2, "decision", "Decision title"),
    ];

    render_memory_index(&mut output, &memories);

    assert!(output.contains("Decision title"));
    assert!(!output.contains("Lesson title"));
    assert!(!output.contains("**Lessons**"));
}

#[test]
fn render_lessons_respects_item_and_char_limits() {
    let mut output = String::new();
    let lessons = vec![
        sample_lesson(1, "First lesson", 0.9, 3),
        sample_lesson(2, "Second lesson", 0.8, 1),
    ];

    let rendered = render_lessons_with_limit(&mut output, &lessons, 1, 240);

    assert_eq!(rendered, 1);
    assert!(output.contains("## Lessons"));
    assert!(output.contains("First lesson"));
    assert!(output.contains("reinforced 3"));
    assert!(!output.contains("Second lesson"));
    assert!(output.chars().count() <= 240);
}

#[test]
fn render_memory_index_respects_item_limit() {
    let mut output = String::new();
    let limits = ContextLimits {
        memory_index_limit: 2,
        ..ContextLimits::default()
    };
    let memories = vec![
        sample_memory(1, "decision", "Decision one"),
        sample_memory(2, "decision", "Decision two"),
        sample_memory(3, "decision", "Decision three"),
    ];

    render_memory_index_with_limits(&mut output, &memories, &limits);

    assert!(output.contains("Decision one"));
    assert!(output.contains("Decision two"));
    assert!(!output.contains("Decision three"));
}

#[test]
fn render_memory_index_truncates_first_item_to_char_limit() {
    let mut output = String::new();
    let limits = ContextLimits {
        memory_index_char_limit: 48,
        ..ContextLimits::default()
    };
    let long_title = "Decision title that is far too long for the index budget";
    let memories = vec![sample_memory(1, "decision", long_title)];

    let rendered = render_memory_index_with_limits(&mut output, &memories, &limits);
    let body = output.strip_prefix("## Index\n").unwrap().trim_end();

    assert_eq!(rendered, 1);
    assert!(body.chars().count() <= limits.memory_index_char_limit);
    assert!(output.contains("..."));
    assert!(!output.contains(long_title));
}

#[test]
fn render_memory_index_can_skip_core_selected_ids() {
    let mut core_output = String::new();
    let now = chrono::Utc::now().timestamp();
    let memories = vec![
        sample_memory_with_epoch(1, "bugfix", "Core bugfix", now),
        sample_memory_with_epoch(2, "decision", "Index decision", now),
    ];
    let core_summary = render_core_memory_with_limits(
        &mut core_output,
        &memories,
        &ContextLimits {
            core_item_limit: 1,
            ..ContextLimits::default()
        },
    );
    let excluded_ids = core_summary.ids.into_iter().collect();

    let mut index_output = String::new();
    let rendered = render_memory_index_with_limits_excluding(
        &mut index_output,
        &memories,
        &ContextLimits::default(),
        &excluded_ids,
    );

    assert_eq!(rendered, 1);
    assert!(!index_output.contains("Core bugfix"));
    assert!(index_output.contains("Index decision"));
}

#[test]
fn render_workstreams_includes_next_action_when_present() {
    let mut output = String::new();
    let workstreams = vec![WorkStream {
        id: 7,
        project: "demo/project".to_string(),
        title: "Refactor context".to_string(),
        description: None,
        status: WorkStreamStatus::Active,
        progress: None,
        next_action: Some("split renderers".to_string()),
        blockers: None,
        created_at_epoch: 0,
        updated_at_epoch: 0,
        completed_at_epoch: None,
    }];

    render_workstreams(&mut output, &workstreams);

    assert!(output.contains("#7 [active] Refactor context -> split renderers"));
}

#[test]
fn render_workstreams_respects_item_and_char_limits() {
    let mut output = String::new();
    let workstreams = vec![
        sample_workstream(1, "First stream", Some("ship the first fix")),
        sample_workstream(2, "Second stream", Some("ship the second fix")),
        sample_workstream(3, "Third stream", Some("ship the third fix")),
    ];

    render_workstreams_with_limits(&mut output, &workstreams, 2, 200);

    assert!(output.contains("#1 [active] First stream"));
    assert!(output.contains("#2 [active] Second stream"));
    assert!(!output.contains("#3 [active] Third stream"));
    assert!(output.chars().count() <= 200);
}

#[test]
fn render_workstreams_stops_at_char_limit() {
    let mut output = String::new();
    let workstreams = vec![
        sample_workstream(1, "First", Some("fix")),
        sample_workstream(2, "Second", Some("fix")),
    ];

    render_workstreams_with_limits(&mut output, &workstreams, 10, 48);

    assert!(output.contains("#1 [active] First"));
    assert!(!output.contains("#2 [active] Second"));
    assert!(output.chars().count() <= 48);
}

#[test]
fn render_core_memory_prioritizes_higher_score_memories() {
    let mut output = String::new();
    let now = chrono::Utc::now().timestamp();
    let memories = vec![
        sample_memory_with_epoch(1, "discovery", "Lower score", now),
        sample_memory_with_epoch(2, "decision", "Higher score", now),
    ];

    render_core_memory(&mut output, &memories);

    let high_pos = output.find("**#2 Higher score**").unwrap();
    let low_pos = output.find("**#1 Lower score**").unwrap();
    assert!(high_pos < low_pos);
}

#[test]
fn render_core_memory_truncates_first_item_to_char_limit() {
    let mut output = String::new();
    let limits = ContextLimits {
        core_char_limit: 120,
        ..ContextLimits::default()
    };
    let mut long_memory = sample_memory(1, "decision", "Compact title");
    long_memory.text = "x".repeat(500);
    let memories = vec![long_memory];

    render_core_memory_with_limits(&mut output, &memories, &limits);

    let body = output.strip_prefix("## Core\n").unwrap().trim_end();
    assert!(output.chars().count() <= limits.core_char_limit);
    assert!(body.chars().count() <= limits.core_char_limit);
    assert!(output.contains("..."));
}

#[test]
fn render_core_memory_keeps_type_diversity_before_filling_same_type() {
    let mut output = String::new();
    let now = chrono::Utc::now().timestamp();
    let memories = vec![
        sample_memory_with_epoch(1, "decision", "Decision one", now),
        sample_memory_with_epoch(2, "decision", "Decision two", now),
        sample_memory_with_epoch(3, "decision", "Decision three", now),
        sample_memory_with_epoch(4, "discovery", "Operational discovery", now),
    ];

    render_core_memory(&mut output, &memories);

    let discovery_pos = output.find("**#4 Operational discovery**").unwrap();
    let third_decision_pos = output.find("**#3 Decision three**").unwrap();
    assert!(discovery_pos < third_decision_pos);
}

#[test]
fn render_core_memory_does_not_backfill_with_memory_self_diagnostics() {
    let mut output = String::new();
    let now = chrono::Utc::now().timestamp();
    let memories = vec![
        sample_memory_with_epoch(1, "decision", "Memory injection diagnosis", now),
        sample_memory_with_epoch(2, "discovery", "Runtime hook finding", now),
    ];

    render_core_memory(&mut output, &memories);

    let runtime_pos = output.find("**#2 Runtime hook finding**").unwrap();
    assert!(runtime_pos < output.len());
    assert!(!output.contains("Memory injection diagnosis"));
}

#[test]
fn render_core_memory_keeps_stale_decision_out_when_recent_context_is_available() {
    let mut output = String::new();
    let now = chrono::Utc::now().timestamp();
    let stale_epoch = now - 8 * 86400;
    let memories = vec![
        sample_memory_with_epoch(1, "decision", "Recent decision one", now),
        sample_memory_with_epoch(3, "discovery", "Recent discovery one", now),
        sample_memory_with_epoch(4, "discovery", "Recent discovery two", now),
        sample_memory_with_epoch(5, "preference", "Recent preference one", now),
        sample_memory_with_epoch(6, "preference", "Recent preference two", now),
        sample_memory_with_epoch(7, "decision", "Stale landing page decision", stale_epoch),
    ];

    render_core_memory(&mut output, &memories);

    assert!(!output.contains("Stale landing page decision"));
}

#[test]
fn context_stats_footer_reports_budget_scope_and_truncation() {
    let footer = build_context_stats_footer(&ContextRenderStats {
        host: "codex-cli".to_string(),
        branch: Some("fix/context".to_string()),
        hook_source: Some("compact".to_string()),
        total_char_limit: 12_000,
        memories_loaded: 7,
        core: SectionRenderStats {
            count: 2,
            chars: 430,
        },
        lessons: SectionRenderStats {
            count: 1,
            chars: 180,
        },
        index: SectionRenderStats {
            count: 5,
            chars: 800,
        },
        preferences: SectionRenderStats {
            count: 3,
            chars: 240,
        },
        project_preferences: 2,
        global_preferences: 1,
        sessions: SectionRenderStats {
            count: 4,
            chars: 620,
        },
        workstreams: SectionRenderStats {
            count: 1,
            chars: 80,
        },
        owner_counts: Default::default(),
        core_ids: vec![1, 2],
        output_chars: 3_200,
        truncated: true,
    });

    assert!(footer.starts_with("\nLoaded"));
    assert!(footer.contains("├─ Memories: 7 total, 2 core, 1 lessons, 5 indexed"));
    assert!(footer.contains("├─ Preferences: 3 total, 2 project, 1 global"));
    assert!(footer.contains("├─ Sessions: 4"));
    assert!(footer.contains("├─ Workstreams: 1"));
    assert!(footer.contains("└─ Budget: 3200 chars (~800 tokens) / 12000, truncated: yes"));
    assert!(!footer.contains('╮'));
    assert!(!footer.contains('╯'));
    assert!(!footer.contains("owners repo="));
}

#[test]
fn context_header_marks_compact_reload_visibly() {
    let header = build_context_header("/tmp/remem", Some("main"), Some("compact"));

    assert!(header.starts_with("remem context"));
    assert!(header.contains("├─ project: /tmp/remem"));
    assert!(header.contains("├─ branch: main"));
    assert!(header.contains("├─ source: compact"));
    assert!(header.contains("└─ updated: "));
    assert!(!header.contains('╮'));
    assert!(!header.contains('╯'));
}

#[test]
fn empty_context_marks_compact_reload_visibly() {
    let output = empty_context_output(&ContextRequest {
        cwd: "/tmp/remem".to_string(),
        project: "/tmp/remem".to_string(),
        session_id: None,
        hook_source: Some("compact".to_string()),
        current_branch: Some("main".to_string()),
        host: HostKind::CodexCli,
        use_colors: false,
    });

    assert!(output.starts_with("remem context"));
    assert!(output.contains("├─ source: compact"));
    assert!(output.contains("Codex compacted the chat, so remem refreshed memory context."));
    assert!(output.contains("No previous sessions found."));
}

#[test]
fn empty_context_marks_clear_reload_visibly() {
    let output = empty_context_output(&ContextRequest {
        cwd: "/tmp/remem".to_string(),
        project: "/tmp/remem".to_string(),
        session_id: None,
        hook_source: Some("clear".to_string()),
        current_branch: Some("main".to_string()),
        host: HostKind::CodexCli,
        use_colors: false,
    });

    assert!(output.starts_with("remem context"));
    assert!(output.contains("├─ source: clear"));
    assert!(output.contains("Context was reloaded after an explicit clear."));
    assert!(output.contains("No previous sessions found."));
}

#[test]
fn empty_context_uses_ansi_when_color_enabled() {
    let output = empty_context_output(&ContextRequest {
        cwd: "/tmp/remem".to_string(),
        project: "/tmp/remem".to_string(),
        session_id: None,
        hook_source: Some("compact".to_string()),
        current_branch: Some("main".to_string()),
        host: HostKind::CodexCli,
        use_colors: true,
    });

    assert!(output.starts_with("\x1b[1;36mremem context\x1b[0m"));
    assert!(output.contains("\x1b[1;36mremem context\x1b[0m"));
    assert!(output.contains("├─ \x1b[1mproject\x1b[0m: /tmp/remem"));
}

#[test]
fn codex_colored_header_aligns_rows_under_hook_context_value() {
    let output = empty_context_output(&ContextRequest {
        cwd: "/tmp/remem".to_string(),
        project: "/tmp/remem".to_string(),
        session_id: None,
        hook_source: None,
        current_branch: Some("main".to_string()),
        host: HostKind::CodexCli,
        use_colors: true,
    });
    let plain = super::super::style::strip_ansi(&output);
    let mut lines = plain.lines();

    assert_eq!(lines.next(), Some("remem context"));
    let project_line = lines.next().unwrap_or_default();
    assert!(project_line.ends_with("├─ project: /tmp/remem"));
    let row_indent = project_line.chars().take_while(|ch| *ch == ' ').count();
    assert_eq!(row_indent, "hook context: ".chars().count());
}

#[test]
fn render_context_output_exposes_lesson_query_failures() {
    let _data_dir = crate::db::test_support::ScopedTestDataDir::new("context-lesson-error");
    let conn = crate::db::test_support::runtime_connection().unwrap();
    conn.execute("DROP TABLE memory_lessons", []).unwrap();
    drop(conn);

    let rendered = render_context_output(
        &ContextRequest {
            cwd: "/tmp/remem".to_string(),
            project: "/tmp/remem".to_string(),
            session_id: None,
            hook_source: Some("session_start".to_string()),
            current_branch: Some("main".to_string()),
            host: HostKind::CodexCli,
            use_colors: false,
        },
        false,
    )
    .unwrap();

    assert!(rendered.output.contains("## Context Load Errors"));
    assert!(rendered
        .output
        .contains("- lessons: failed to load lessons for /tmp/remem"));
    assert!(!rendered.output.contains("No previous sessions found."));
}

#[test]
fn render_context_output_exposes_primary_memory_query_failures() {
    let _data_dir = crate::db::test_support::ScopedTestDataDir::new("context-memory-error");
    let conn = crate::db::test_support::runtime_connection().unwrap();
    conn.execute("DROP TABLE memories", []).unwrap();
    drop(conn);

    let rendered = render_context_output(
        &ContextRequest {
            cwd: "/tmp/remem".to_string(),
            project: "/tmp/remem".to_string(),
            session_id: None,
            hook_source: Some("session_start".to_string()),
            current_branch: Some("main".to_string()),
            host: HostKind::CodexCli,
            use_colors: false,
        },
        false,
    )
    .unwrap();

    assert!(rendered.output.contains("## Context Load Errors"));
    assert!(rendered
        .output
        .contains("- memories: failed to load recent context memories for /tmp/remem"));
    assert!(rendered
        .output
        .contains("- memories: failed to search context memories for /tmp/remem"));
    assert!(!rendered.output.contains("No previous sessions found."));
}

#[test]
fn strict_context_pipeline_opens_one_database_connection() -> anyhow::Result<()> {
    let data_dir = crate::db::test_support::ScopedTestDataDir::new("context-single-db-open");
    let setup = crate::db::test_support::runtime_connection()?;
    drop(setup);

    crate::db::test_support::reset_runtime_connection_open_count();
    let cwd = data_dir.path.to_string_lossy().to_string();
    let transcript_path = data_dir.path.join("transcript.jsonl");
    let invocation = ContextInvocation {
        cwd: cwd.clone(),
        project: crate::db::project_from_cwd(&cwd),
        session_id: Some("sess-single-db-open".to_string()),
        transcript_path: Some(transcript_path.to_string_lossy().to_string()),
        source: Some("session_start".to_string()),
        host: HostKind::CodexCli,
        use_colors: false,
        debug: false,
        force: false,
        gate_mode: Some("strict".to_string()),
    };

    generate_context_for_test(invocation, true)?;

    assert_eq!(crate::db::test_support::runtime_connection_open_count(), 1);
    Ok(())
}

fn sample_lesson(id: i64, title: &str, confidence: f64, reinforcement_count: i64) -> LessonMemory {
    LessonMemory {
        memory: sample_memory(id, "lesson", title),
        metadata: LessonMetadata {
            memory_id: id,
            confidence,
            reinforcement_count,
            source_evidence: None,
            last_reinforced_at_epoch: 1_710_000_000,
            stale_after_epoch: None,
        },
    }
}
