use camino::{Utf8Path, Utf8PathBuf};
use chrono::Datelike;
use dirigent_anth::{
    correlation::correlate_tools,
    dedup::dedup_messages,
    noise::{classify_noise, NoiseKind},
    parse_session,
    tree::ConversationTree,
    types::{ContentBlock, RawMessage},
    util::parse_timestamp,
};

#[test]
fn parse_minimal_session() {
    let path = Utf8Path::new("tests/fixtures/minimal_session.jsonl");
    let messages = parse_session(path).unwrap();

    assert_eq!(messages.len(), 6, "Expected 6 messages, got {}", messages.len());

    let type_names: Vec<&str> = messages
        .iter()
        .map(|m| match m {
            RawMessage::User(_) => "user",
            RawMessage::Assistant(_) => "assistant",
            RawMessage::Progress(_) => "progress",
            RawMessage::System(_) => "system",
            RawMessage::QueueOperation(_) => "queue-operation",
            RawMessage::FileHistorySnapshot(_) => "file-history-snapshot",
            RawMessage::LastPrompt(_) => "last-prompt",
        })
        .collect();

    assert_eq!(
        type_names.iter().filter(|&&t| t == "queue-operation").count(),
        2
    );
    assert_eq!(type_names.iter().filter(|&&t| t == "user").count(), 2);
    assert_eq!(
        type_names.iter().filter(|&&t| t == "assistant").count(),
        2
    );
}

#[test]
fn parse_line_returns_none_for_invalid_json() {
    assert!(dirigent_anth::parse_line("not valid json", 1).is_none());
    assert!(dirigent_anth::parse_line("{}", 1).is_none());
}

#[test]
fn dedup_streaming_session() {
    let path = Utf8Path::new("tests/fixtures/streaming_dedup.jsonl");
    let messages = parse_session(path).unwrap();

    // Raw should have 6 lines (including 3 versions of same assistant message)
    assert_eq!(messages.len(), 6, "Raw messages: expected 6, got {}", messages.len());

    let deduped = dedup_messages(messages);

    // After dedup: U1, A1(final), U2, A2 = 4
    assert_eq!(deduped.len(), 4, "Deduped messages: expected 4, got {}", deduped.len());

    // The kept assistant message must be the final version
    let first_assistant = deduped.iter().find(|m| matches!(m, RawMessage::Assistant(_))).unwrap();
    if let RawMessage::Assistant(a) = first_assistant {
        assert!(a.message.stop_reason.is_some(), "Deduped assistant should have stop_reason set");
        assert_eq!(a.message.stop_reason.as_deref(), Some("tool_use"));
        assert_eq!(a.message.content.len(), 2, "Final version should have 2 content blocks");
    } else {
        unreachable!();
    }
}

#[test]
fn dedup_preserves_non_streamed_messages() {
    let path = Utf8Path::new("tests/fixtures/minimal_session.jsonl");
    let messages = parse_session(path).unwrap();
    let count_before = messages.len();
    let deduped = dedup_messages(messages);
    // No streaming in minimal_session, so count should be same
    assert_eq!(deduped.len(), count_before);
}

#[test]
fn correlate_parallel_tools() {
    let path = Utf8Path::new("tests/fixtures/tool_correlation.jsonl");
    let messages = dirigent_anth::parse_session_deduped(path).unwrap();
    let exchanges = correlate_tools(&messages);

    // 3 tool calls: 2 parallel (Bash+Read) + 1 sequential (Write)
    assert_eq!(exchanges.len(), 3);

    // All should have results
    assert!(exchanges.iter().all(|e| e.result.is_some()));

    // Verify correct pairing by ID
    for ex in &exchanges {
        assert_eq!(ex.call.id, ex.result.as_ref().unwrap().tool_use_id);
    }
}

#[test]
fn correlate_no_tools_returns_empty() {
    // Test with just a plain user message — no tool calls or results
    let messages = vec![
        serde_json::from_str::<RawMessage>(
            r#"{"type":"user","uuid":"x","timestamp":"2026-01-01T00:00:00Z","sessionId":"s","message":{"role":"user","content":"hello"}}"#,
        )
        .unwrap(),
    ];
    let exchanges = correlate_tools(&messages);
    assert!(exchanges.is_empty());
}

#[test]
fn build_branching_tree() {
    let path = Utf8Path::new("tests/fixtures/branching_tree.jsonl");
    let messages = dirigent_anth::parse_session(path).unwrap();
    let tree = ConversationTree::build(&messages);

    assert_eq!(tree.roots.len(), 1);
    assert!(!tree.is_linear());
    assert_eq!(tree.branch_points().len(), 1); // A1 has 2 children

    let main = tree.main_thread();
    assert_eq!(main.len(), 4); // R → A1 → U2 → A3 (first branch)
}

#[test]
fn linear_conversation_is_linear() {
    let path = Utf8Path::new("tests/fixtures/minimal_session.jsonl");
    let messages = dirigent_anth::parse_session(path).unwrap();
    let tree = ConversationTree::build(&messages);
    assert!(tree.is_linear());
}

#[test]
fn classify_noise_from_fixture() {
    let path = Utf8Path::new("tests/fixtures/noise_patterns.jsonl");
    let messages = dirigent_anth::parse_session(path).unwrap();

    assert_eq!(messages.len(), 9, "Expected 9 messages in noise fixture");

    let classifications: Vec<Option<NoiseKind>> = messages.iter()
        .map(classify_noise)
        .collect();

    assert_eq!(classifications[0], Some(NoiseKind::QueueOp));
    assert_eq!(classifications[1], Some(NoiseKind::Meta));
    assert_eq!(classifications[2], Some(NoiseKind::Warmup));
    assert_eq!(classifications[3], Some(NoiseKind::Interrupted));
    assert_eq!(classifications[4], Some(NoiseKind::Continuation));
    assert_eq!(classifications[5], Some(NoiseKind::ApiError));
    assert_eq!(classifications[6], Some(NoiseKind::SystemCaveat));
    assert_eq!(classifications[7], None); // normal user
    assert_eq!(classifications[8], None); // normal assistant
}

#[test]
fn load_subagent_from_fixture() {
    let artifacts_dir = Utf8Path::new("tests/fixtures/subagent/parent");
    let subagents = dirigent_anth::load_subagents(artifacts_dir).unwrap();

    assert_eq!(subagents.len(), 1);
    assert_eq!(subagents[0].agent_id, "abc123");
    assert_eq!(subagents[0].meta.agent_type.as_deref(), Some("Explore"));
    assert_eq!(subagents[0].messages.len(), 2);
}

#[test]
fn load_subagents_empty_dir() {
    // Non-existent artifacts dir should return empty vec
    let artifacts_dir = Utf8Path::new("tests/fixtures/nonexistent");
    let subagents = dirigent_anth::load_subagents(artifacts_dir).unwrap();
    assert!(subagents.is_empty());
}

#[test]
fn load_full_session_with_subagents() {
    use dirigent_anth::types::SessionRef;

    let session_ref = SessionRef {
        id: "parent".to_string(),
        jsonl_path: Utf8PathBuf::from("tests/fixtures/subagent/parent.jsonl"),
        artifacts_dir: Some(Utf8PathBuf::from("tests/fixtures/subagent/parent")),
        index_entry: None,
    };

    let session = dirigent_anth::load_session(&session_ref).unwrap();
    assert!(!session.messages.is_empty());
    assert!(!session.subagents.is_empty());
    assert!(!session.tree.roots.is_empty());
    assert!(!session.tool_exchanges.is_empty());
}

#[test]
fn load_session_without_artifacts() {
    use dirigent_anth::types::SessionRef;

    let session_ref = SessionRef {
        id: "minimal".to_string(),
        jsonl_path: Utf8PathBuf::from("tests/fixtures/minimal_session.jsonl"),
        artifacts_dir: None,
        index_entry: None,
    };

    let session = dirigent_anth::load_session(&session_ref).unwrap();
    assert_eq!(session.messages.len(), 6); // 2 queue-ops + 2 users + 2 assistants
    assert!(session.subagents.is_empty());
    assert!(session.tree.is_linear());
}

#[test]
fn content_as_string_or_blocks() {
    // String content
    let s: dirigent_anth::types::Content = serde_json::from_str(r#""hello""#).unwrap();
    assert!(matches!(s, dirigent_anth::types::Content::Text(_)));

    // Block content
    let b: dirigent_anth::types::Content =
        serde_json::from_str(r#"[{"type":"text","text":"hi"}]"#).unwrap();
    assert!(matches!(b, dirigent_anth::types::Content::Blocks(_)));

    // Empty blocks
    let empty: dirigent_anth::types::Content = serde_json::from_str(r#"[]"#).unwrap();
    assert!(matches!(empty, dirigent_anth::types::Content::Blocks(ref v) if v.is_empty()));
}

#[test]
fn missing_optional_fields_dont_crash() {
    // Minimal assistant message with many fields missing
    let json = r#"{
        "type": "assistant",
        "message": {
            "content": [{"type": "text", "text": "hi"}]
        }
    }"#;
    let msg: RawMessage = serde_json::from_str(json).unwrap();
    assert!(matches!(msg, RawMessage::Assistant(_)));
}

#[test]
fn tool_result_content_string_and_blocks() {
    // tool_result with string content
    let json = r#"{"type":"tool_result","tool_use_id":"t1","content":"output text","is_error":false}"#;
    let block: ContentBlock = serde_json::from_str(json).unwrap();
    if let ContentBlock::ToolResult { content, is_error, .. } = block {
        assert!(!is_error);
        assert!(content.is_some());
    } else {
        panic!("Expected ToolResult");
    }

    // tool_result with no content
    let json2 = r#"{"type":"tool_result","tool_use_id":"t2"}"#;
    let block2: ContentBlock = serde_json::from_str(json2).unwrap();
    if let ContentBlock::ToolResult { content, is_error, .. } = block2 {
        assert!(!is_error);
        assert!(content.is_none());
    } else {
        panic!("Expected ToolResult");
    }
}

#[test]
fn extra_unknown_fields_are_ignored() {
    // Messages with extra fields not in our structs should parse fine
    let json = r#"{
        "type": "user",
        "uuid": "x",
        "timestamp": "2026-01-01T00:00:00Z",
        "sessionId": "s",
        "unknownField": "should be ignored",
        "anotherExtra": 42,
        "message": {"role": "user", "content": "hello"}
    }"#;
    let msg: RawMessage = serde_json::from_str(json).unwrap();
    assert!(matches!(msg, RawMessage::User(_)));
}

#[test]
fn timestamp_parsing_all_formats() {
    // ISO 8601
    let iso = parse_timestamp(&serde_json::json!("2026-03-22T17:00:13.192Z")).unwrap();
    assert_eq!(iso.year(), 2026);

    // Unix millis
    let ms = parse_timestamp(&serde_json::json!(1769461914249_i64)).unwrap();
    assert!(ms.year() >= 2025);

    // Unix seconds
    let secs = parse_timestamp(&serde_json::json!(1769461914_i64)).unwrap();
    assert!(secs.year() >= 2025);
}
