use super::{
    SymbolMatch, call_hierarchy_matches_from_incoming_response,
    call_hierarchy_matches_from_outgoing_response, document_symbol_matches_from_response,
    function_matches_from_document_response, is_function_symbol_kind,
    location_matches_from_response, location_matches_from_response_with_full_content,
    prepare_call_hierarchy_response, symbol_matches_from_response,
};
use crate::lsp::{SourceCache, symbol_full_content_from_document_response};
use crate::test_support::TestDir;
use lsp_types::SymbolKind;
use serde_json::json;
use url::Url;

struct SourceFixture {
    _dir: TestDir,
    file: std::path::PathBuf,
    uri: String,
    cache: SourceCache,
}

fn matched(
    path: &std::path::Path,
    line: u32,
    col: u32,
    name: &str,
    kind: SymbolKind,
    line_content: &str,
) -> SymbolMatch {
    SymbolMatch {
        name: name.to_string(),
        kind,
        path: path.to_path_buf(),
        line,
        col,
        line_content: line_content.to_string(),
        full_content: None,
    }
}

fn source_fixture(contents: &str) -> SourceFixture {
    let dir = TestDir::new("symbols");
    let file = dir.write_file("src/lib.rs", contents);
    let uri = Url::from_file_path(&file)
        .expect("file path should become URI")
        .to_string();

    SourceFixture {
        _dir: dir,
        file,
        uri,
        cache: SourceCache::default(),
    }
}

#[test]
fn returns_placeholder_for_missing_line() {
    let dir = TestDir::new("symbols");
    let file = dir.write_file("main.rs", "fn main() {}\n");
    let mut cache = SourceCache::default();

    assert_eq!(cache.line_content(&file, 99), "<line unavailable>");
}

#[test]
fn parses_workspace_symbol_locations() {
    let fixture = source_fixture("first line\nsecond line\n");

    let matches = symbol_matches_from_response(&json!([
        {
            "name": "symbol",
            "kind": 12,
            "location": {
                "uri": fixture.uri,
                "range": {
                    "start": { "line": 1, "character": 2 },
                    "end": { "line": 1, "character": 8 }
                }
            }
        }
    ]))
    .expect("response should parse");

    assert_eq!(
        matches,
        vec![SymbolMatch {
            name: "symbol".to_string(),
            kind: SymbolKind::FUNCTION,
            path: fixture.file,
            line: 2,
            col: 3,
            line_content: "second line".to_string(),
            full_content: None,
        }]
    );
}

#[test]
fn identifies_function_like_symbol_kinds() {
    assert!(is_function_symbol_kind(SymbolKind::METHOD));
    assert!(is_function_symbol_kind(SymbolKind::CONSTRUCTOR));
    assert!(is_function_symbol_kind(SymbolKind::FUNCTION));
    assert!(is_function_symbol_kind(SymbolKind::OPERATOR));
    assert!(!is_function_symbol_kind(SymbolKind::CLASS));
    assert!(!is_function_symbol_kind(SymbolKind::VARIABLE));
}

#[test]
fn parses_document_symbols_for_functions() {
    let mut fixture =
        source_fixture("struct S;\nfn first() {}\nimpl S {\n    fn second(&self) {}\n}\n");

    let matches = function_matches_from_document_response(
        &json!([
            {
                "name": "S",
                "kind": 23,
                "range": {
                    "start": { "line": 0, "character": 0 },
                    "end": { "line": 3, "character": 1 }
                },
                "selectionRange": {
                    "start": { "line": 0, "character": 7 },
                    "end": { "line": 0, "character": 8 }
                },
                "children": [
                    {
                        "name": "second",
                        "kind": 6,
                        "range": {
                            "start": { "line": 3, "character": 0 },
                            "end": { "line": 3, "character": 23 }
                        },
                        "selectionRange": {
                            "start": { "line": 3, "character": 7 },
                            "end": { "line": 3, "character": 13 }
                        }
                    }
                ]
            },
            {
                "name": "first",
                "kind": 12,
                "range": {
                    "start": { "line": 1, "character": 0 },
                    "end": { "line": 1, "character": 13 }
                },
                "selectionRange": {
                    "start": { "line": 1, "character": 3 },
                    "end": { "line": 1, "character": 8 }
                }
            }
        ]),
        &fixture.file,
        &mut fixture.cache,
    )
    .expect("document symbols should parse");

    assert_eq!(
        matches,
        vec![
            SymbolMatch {
                name: "second".to_string(),
                kind: SymbolKind::METHOD,
                path: fixture.file.clone(),
                line: 4,
                col: 8,
                line_content: "    fn second(&self) {}".to_string(),
                full_content: None,
            },
            SymbolMatch {
                name: "first".to_string(),
                kind: SymbolKind::FUNCTION,
                path: fixture.file,
                line: 2,
                col: 4,
                line_content: "fn first() {}".to_string(),
                full_content: None,
            },
        ]
    );
}

#[test]
fn parses_document_symbols_for_all_kinds() {
    let mut fixture = source_fixture("struct S;\nfn first() {}\n");

    let matches = document_symbol_matches_from_response(
        &json!([
            {
                "name": "S",
                "kind": 23,
                "range": {
                    "start": { "line": 0, "character": 0 },
                    "end": { "line": 0, "character": 8 }
                },
                "selectionRange": {
                    "start": { "line": 0, "character": 7 },
                    "end": { "line": 0, "character": 8 }
                }
            },
            {
                "name": "first",
                "kind": 12,
                "range": {
                    "start": { "line": 1, "character": 0 },
                    "end": { "line": 1, "character": 13 }
                },
                "selectionRange": {
                    "start": { "line": 1, "character": 3 },
                    "end": { "line": 1, "character": 8 }
                }
            }
        ]),
        &fixture.file,
        &mut fixture.cache,
    )
    .expect("document symbols should parse");

    assert_eq!(matches.len(), 2);
    assert_eq!(matches[0].name, "S");
    assert_eq!(matches[1].name, "first");
}

#[test]
fn parses_flat_document_symbols_at_symbol_name() {
    let mut fixture =
        source_fixture("pub fn format_order(order: &str) -> String { order.to_string() }\n");

    let matches = document_symbol_matches_from_response(
        &json!([
            {
                "name": "format_order",
                "kind": 12,
                "location": {
                    "uri": fixture.uri,
                    "range": {
                        "start": { "line": 0, "character": 0 },
                        "end": { "line": 0, "character": 62 }
                    }
                }
            }
        ]),
        &fixture.file,
        &mut fixture.cache,
    )
    .expect("document symbols should parse");

    assert_eq!(matches[0].line, 1);
    assert_eq!(matches[0].col, 8);
    assert_eq!(
        matches[0].line_content,
        "pub fn format_order(order: &str) -> String { order.to_string() }"
    );
}

#[test]
fn parses_flat_document_symbols_inside_attributed_ranges() {
    let mut fixture = source_fixture("#[test]\nfn parses_callers_arguments() {}\n");

    let matches = function_matches_from_document_response(
        &json!([
            {
                "name": "parses_callers_arguments",
                "kind": 12,
                "location": {
                    "uri": fixture.uri,
                    "range": {
                        "start": { "line": 0, "character": 0 },
                        "end": { "line": 1, "character": 32 }
                    }
                }
            }
        ]),
        &fixture.file,
        &mut fixture.cache,
    )
    .expect("document symbols should parse");

    assert_eq!(matches[0].line, 2);
    assert_eq!(matches[0].col, 4);
    assert_eq!(matches[0].line_content, "fn parses_callers_arguments() {}");
}

#[test]
fn parses_location_links() {
    let mut fixture = source_fixture("first line\nsecond line\n");

    let matches = location_matches_from_response(
        &json!([
            {
                "targetUri": fixture.uri,
                "targetRange": {
                    "start": { "line": 1, "character": 0 },
                    "end": { "line": 1, "character": 11 }
                },
                "targetSelectionRange": {
                    "start": { "line": 1, "character": 2 },
                    "end": { "line": 1, "character": 8 }
                }
            }
        ]),
        "symbol",
        SymbolKind::FUNCTION,
        &mut fixture.cache,
    )
    .expect("location links should parse");

    assert_eq!(matches[0].line, 2);
    assert_eq!(matches[0].col, 3);
}

#[test]
fn keeps_full_content_from_definition_ranges() {
    let mut fixture =
        source_fixture("# attached comment\ndef build_sample_order():\n    return 1\n");

    let matches = location_matches_from_response_with_full_content(
        &json!([
            {
                "uri": fixture.uri,
                "range": {
                    "start": { "line": 1, "character": 0 },
                    "end": { "line": 2, "character": 12 }
                }
            }
        ]),
        "build_sample_order",
        SymbolKind::FUNCTION,
        &mut fixture.cache,
    )
    .expect("location response should parse");

    assert_eq!(
        matches[0].full_content.as_deref(),
        Some("# attached comment\ndef build_sample_order():\n    return 1")
    );
}

#[test]
fn extracts_function_body_from_nested_document_symbols() {
    let mut fixture = source_fixture("def build_sample_order() -> int:\n    return 1\n");
    let target = matched(
        &fixture.file,
        1,
        1,
        "build_sample_order",
        SymbolKind::FUNCTION,
        "def build_sample_order() -> int:",
    );

    let content = symbol_full_content_from_document_response(
        &json!([
            {
                "name": "build_sample_order",
                "kind": 12,
                "range": {
                    "start": { "line": 0, "character": 0 },
                    "end": { "line": 1, "character": 12 }
                },
                "selectionRange": {
                    "start": { "line": 0, "character": 4 },
                    "end": { "line": 0, "character": 22 }
                }
            }
        ]),
        &fixture.file,
        &target,
        &mut fixture.cache,
    )
    .expect("document symbols should parse");

    assert_eq!(
        content.as_deref(),
        Some("def build_sample_order() -> int:\n    return 1")
    );
}

#[test]
fn extracts_class_body_with_attributes_from_nested_document_symbols() {
    let mut fixture = source_fixture(
        "# some class\n@dataclass(slots=True)\nclass Order:\n    customer: str\n    items: list[str]\n\n    def total(self) -> int:\n        return len(self.items)\n",
    );
    let target = matched(
        &fixture.file,
        3,
        7,
        "Order",
        SymbolKind::CLASS,
        "class Order:",
    );

    let content = symbol_full_content_from_document_response(
        &json!([
            {
                "name": "Order",
                "kind": 5,
                "range": {
                    "start": { "line": 1, "character": 0 },
                    "end": { "line": 7, "character": 30 }
                },
                "selectionRange": {
                    "start": { "line": 2, "character": 6 },
                    "end": { "line": 2, "character": 11 }
                },
                "children": [
                    {
                        "name": "customer",
                        "kind": 8,
                        "range": {
                            "start": { "line": 3, "character": 4 },
                            "end": { "line": 3, "character": 17 }
                        },
                        "selectionRange": {
                            "start": { "line": 3, "character": 4 },
                            "end": { "line": 3, "character": 12 }
                        }
                    }
                ]
            }
        ]),
        &fixture.file,
        &target,
        &mut fixture.cache,
    )
    .expect("document symbols should parse");

    assert_eq!(
        content.as_deref(),
        Some(
            "# some class\n@dataclass(slots=True)\nclass Order:\n    customer: str\n    items: list[str]\n\n    def total(self) -> int:\n        return len(self.items)"
        )
    );
}

#[test]
fn parses_prepare_call_hierarchy_response() {
    let items = prepare_call_hierarchy_response(&json!([
        {
            "name": "main",
            "kind": 12,
            "uri": "file:///tmp/main.rs",
            "range": {
                "start": { "line": 0, "character": 0 },
                "end": { "line": 0, "character": 10 }
            },
            "selectionRange": {
                "start": { "line": 0, "character": 3 },
                "end": { "line": 0, "character": 7 }
            }
        }
    ]))
    .expect("call hierarchy items should parse");

    assert_eq!(items.len(), 1);
}

#[test]
fn parses_call_hierarchy_incoming_calls() {
    let mut fixture = source_fixture("fn caller() {}\n");

    let matches = call_hierarchy_matches_from_incoming_response(
        &json!([
            {
                "from": {
                    "name": "caller",
                    "kind": 12,
                    "uri": fixture.uri,
                    "range": {
                        "start": { "line": 0, "character": 0 },
                        "end": { "line": 0, "character": 13 }
                    },
                    "selectionRange": {
                        "start": { "line": 0, "character": 3 },
                        "end": { "line": 0, "character": 9 }
                    }
                },
                "fromRanges": []
            }
        ]),
        &mut fixture.cache,
    )
    .expect("incoming calls should parse");

    assert_eq!(matches[0].name, "caller");
}

#[test]
fn parses_call_hierarchy_outgoing_calls() {
    let mut fixture = source_fixture("fn callee() {}\n");

    let matches = call_hierarchy_matches_from_outgoing_response(
        &json!([
            {
                "to": {
                    "name": "callee",
                    "kind": 12,
                    "uri": fixture.uri,
                    "range": {
                        "start": { "line": 0, "character": 0 },
                        "end": { "line": 0, "character": 13 }
                    },
                    "selectionRange": {
                        "start": { "line": 0, "character": 3 },
                        "end": { "line": 0, "character": 9 }
                    }
                },
                "fromRanges": []
            }
        ]),
        &mut fixture.cache,
    )
    .expect("outgoing calls should parse");

    assert_eq!(matches[0].name, "callee");
}
