// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 The Gleam contributors

use lsp_types::{
    Contents, Hover, HoverParams, MarkedString, Position, Range, WorkDoneProgressParams,
};

use super::*;

fn hover(tester: TestProject<'_>, position: Position) -> Option<Hover> {
    tester.at(position, |engine, param, _| {
        let params = HoverParams {
            text_document_position_params: param,
            work_done_progress_params: WorkDoneProgressParams::default(),
        };
        let response = engine.hover(params);

        response.result.unwrap()
    })
}

pub fn show_hover(code: &str, range: Range, position: Position) -> String {
    let Range { start, end } = range;

    // When we display the over range the end character is always excluded!
    let end = Position::new(end.line, end.character);

    let mut buffer: String = "".into();
    for (line_number, line) in code.lines().enumerate() {
        let mut underline: String = "".into();
        let mut underline_empty = true;

        for (column_number, _) in line.chars().enumerate() {
            let current_position = Position::new(line_number as u32, column_number as u32);
            if current_position == position {
                underline_empty = false;
                underline.push('↑');
            } else if start <= current_position && current_position < end {
                underline_empty = false;
                underline.push('▔');
            } else {
                underline.push(' ');
            }
        }

        buffer.push_str(line);
        if !underline_empty {
            buffer.push('\n');
            buffer.push_str(&underline);
        }
        buffer.push('\n');
    }

    buffer
}

fn pretty_hover_contents(contents: Contents) -> String {
    let (kind, content) = match contents {
        Contents::MarkedString(marked_string) => ("markdown", pretty_marked_string(marked_string)),
        Contents::MarkedStringList(marked_strings) => (
            "markdown array",
            marked_strings
                .into_iter()
                .map(pretty_marked_string)
                .join("\n\n"),
        ),
        Contents::MarkupContent(lsp_types::MarkupContent { kind, value }) => match kind {
            lsp_types::MarkupKind::PlainText => ("plaintext", value),
            lsp_types::MarkupKind::Markdown => ("markdown", value),
        },
    };
    format!("----- Hover content ({kind}) -----\n{content}")
}

fn pretty_marked_string(marked_string: MarkedString) -> String {
    match marked_string {
        MarkedString::String(string) => string,
        #[allow(deprecated)]
        MarkedString::MarkedStringWithLanguage(lsp_types::MarkedStringWithLanguage {
            language,
            value,
        }) => {
            format!("```{language}\n{value}\n```")
        }
    }
}

#[macro_export]
macro_rules! assert_hover {
    ($code:literal, $position:expr $(,)?) => {
        let project = TestProject::for_source($code);
        assert_hover!(project, $position);
    };

    ($project:expr, $position:expr $(,)?) => {
        let src = $project.src;
        let position = $position.find_position(src);
        let result = hover($project, position).expect("no hover produced");
        let pretty_hover = show_hover(src, result.range.expect("hover with no range"), position);
        let output = format!(
            "{}\n\n{}",
            pretty_hover,
            pretty_hover_contents(result.contents)
        );
        insta::assert_snapshot!(insta::internals::AutoName, output, src);
    };
}

#[test]
fn hover_function_definition() {
    assert_hover!(
        "
fn add_2(x) {
  x + 2
}
",
        find_position_of("add_2")
    );
}

#[test]
fn hover_local_function() {
    assert_hover!(
        "
fn my_fn() {
  Nil
}

fn main() {
  my_fn
}
",
        find_position_of("my_fn").under_char('y').nth_occurrence(2)
    );
}

// https://github.com/gleam-lang/gleam/issues/2654
#[test]
fn hover_local_function_in_pipe() {
    assert_hover!(
        "
fn add1(num: Int) -> Int {
  num + 1
}

pub fn main() {
  add1(1)

  1
  |> add1
  |> add1
  |> add1
}
",
        find_position_of("add1")
            .with_char_offset(1)
            .nth_occurrence(2)
    );
}

// https://github.com/gleam-lang/gleam/issues/2654
#[test]
fn hover_local_function_in_pipe_1() {
    assert_hover!(
        "
fn add1(num: Int) -> Int {
  num + 1
}

pub fn main() {
  add1(1)

  1
  |> add1
  |> add1
  |> add1
}
",
        find_position_of("add1")
            .with_char_offset(2)
            .nth_occurrence(3)
    );
}

// https://github.com/gleam-lang/gleam/issues/2654
#[test]
fn hover_local_function_in_pipe_2() {
    assert_hover!(
        "
fn add1(num: Int) -> Int {
  num + 1
}

pub fn main() {
  add1(1)

  1
  |> add1
  |> add1
  |> add1
}
",
        find_position_of("add1")
            .with_char_offset(2)
            .nth_occurrence(4)
    );
}

// https://github.com/gleam-lang/gleam/issues/2654
#[test]
fn hover_local_function_in_pipe_3() {
    assert_hover!(
        "
fn add1(num: Int) -> Int {
  num + 1
}

pub fn main() {
  add1(1)

  1
  |> add1
  |> add1
  |> add1
}
",
        find_position_of("add1")
            .with_char_offset(2)
            .nth_occurrence(5)
    );
}

#[test]
fn hover_imported_function() {
    let code = "
import example_module
fn main() {
  example_module.my_fn
}
";

    assert_hover!(
        TestProject::for_source(code).add_module("example_module", "pub fn my_fn() { Nil }"),
        find_position_of("my_fn").under_char('_'),
    );
}

#[test]
fn hover_external_imported_function() {
    let code = "
import example_module
fn main() {
  example_module.my_fn
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("example_module", "pub fn my_fn() { Nil }"),
        find_position_of("my_fn").under_char('_'),
    );
}

#[test]
fn hover_external_imported_unqualified_function() {
    let code = "
import example_module.{my_fn}
fn main() {
  my_fn
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("example_module", "pub fn my_fn() { Nil }"),
        find_position_of("my_fn").under_char('f').nth_occurrence(2),
    );
}

#[test]
fn hover_external_imported_function_renamed_module() {
    let code = "
import example_module as renamed_module
fn main() {
    renamed_module.my_fn
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("example_module", "pub fn my_fn() { Nil }"),
        find_position_of("my_fn").under_char('f'),
    );
}

#[test]
fn hover_external_unqualified_imported_function_renamed_module() {
    let code = "
import example_module.{my_fn} as renamed_module
fn main() {
    my_fn
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("example_module", "pub fn my_fn() { Nil }"),
        find_position_of("my_fn").under_char('_').nth_occurrence(2),
    );
}

#[test]
fn hover_external_imported_function_nested_module() {
    // Example of HexDocs link with nested modules: https://lustre.hexdocs.pm/lustre/element/svg.html
    let code = "
import my/nested/example_module
fn main() {
    example_module.my_fn
}
";

    assert_hover!(
        TestProject::for_source(code)
            .add_hex_module("my/nested/example_module", "pub fn my_fn() { Nil }"),
        find_position_of("my_fn").under_char('f'),
    );
}

#[test]
fn hover_external_imported_ffi_renamed_function() {
    let code = r#"
import example_module
fn main() {
    example_module.my_fn
}
"#;

    let hex_module = r#"
@external(erlang, "my_mod_ffi", "renamed_fn")
pub fn my_fn() -> Nil
"#;

    assert_hover!(
        TestProject::for_source(code).add_hex_module("example_module", hex_module,),
        find_position_of("my_fn").under_char('f'),
    );
}

#[test]
fn hover_external_imported_constants() {
    let code = "
import example_module
fn main() {
  example_module.my_const
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("example_module", "pub const my_const = 42"),
        find_position_of("my_const").under_char('_'),
    );
}

#[test]
fn hover_external_imported_unqualified_constants() {
    let code = "
import example_module.{my_const}
fn main() {
  my_const
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("example_module", "pub const my_const = 42"),
        find_position_of("my_const")
            .under_char('c')
            .nth_occurrence(2),
    );
}

#[test]
fn hover_external_value_with_two_modules_same_name() {
    let code = "
import a/example_module as _
import b/example_module
fn main() {
    example_module.my_const
}
";

    assert_hover!(
        TestProject::for_source(code)
            .add_hex_module("a/example_module", "pub const my_const = 42")
            .add_hex_module("b/example_module", "pub const my_const = 42"),
        find_position_of("my_const").under_char('c'),
    );
}

#[test]
fn hover_external_function_with_another_value_same_name() {
    let code = "
import a/example_module.{my_const as discarded}
import b/example_module.{my_const} as _
fn main() {
    my_const
}
";

    assert_hover!(
        TestProject::for_source(code)
            .add_hex_module("a/example_module", "pub const my_const = 42")
            .add_hex_module("b/example_module", "pub const my_const = 42"),
        find_position_of("my_const")
            .under_char('o')
            .nth_occurrence(3),
    );
}

#[test]
fn hover_function_definition_with_docs() {
    assert_hover!(
        "
/// Exciting documentation
/// Maybe even multiple lines
fn append(x, y) {
  x <> y
}
",
        find_position_of("append")
    );
}

#[test]
fn hover_function_argument() {
    assert_hover!(
        "
/// Exciting documentation
/// Maybe even multiple lines
fn append(x, y) {
  x <> y
}
",
        find_position_of("append(x, y)").under_char('x')
    );
}

#[test]
fn hover_function_body() {
    let code = "
/// Exciting documentation
/// Maybe even multiple lines
fn append(x, y) {
  x <> y
}
";

    assert_eq!(
        hover(TestProject::for_source(code), Position::new(4, 1)),
        None
    );
}

#[test]
fn hover_expressions_in_function_body() {
    assert_hover!(
        "
fn append(x, y) {
  x <> y
}
",
        find_position_of("x").nth_occurrence(2)
    );
}

#[test]
fn hover_module_constant() {
    assert_hover!(
        "
/// Exciting documentation
/// Maybe even multiple lines
const one = 1
",
        find_position_of("one")
    );
}

#[test]
fn hover_variable_in_use_expression() {
    assert_hover!(
        "
fn b(fun: fn(Int) -> String) {
  fun(42)
}

fn do_stuff() {
  let c = \"done\"

  use a <- b
  c
}
",
        find_position_of("use a").under_last_char()
    );
}

#[test]
fn hover_variable_in_use_expression_1() {
    assert_hover!(
        "
fn b(fun: fn(Int) -> String) {
  fun(42)
}

fn do_stuff() {
  let c = \"done\"

  use a <- b
  c
}
",
        find_position_of("b").nth_occurrence(2)
    );
}

#[test]
fn hover_variable_in_use_expression_2() {
    assert_hover!(
        "
fn b(fun: fn(Int) -> String) {
  fun(42)
}

fn do_stuff() {
  let c = \"done\"

  use a <- b
  c
}
",
        find_position_of("c").nth_occurrence(2)
    );
}

#[test]
fn hover_function_arg_annotation_2() {
    assert_hover!(
        "
/// Exciting documentation
/// Maybe even multiple lines
fn append(x: String, y: String) -> String {
  x <> y
}
",
        find_position_of("String").under_char('n')
    );
}

#[test]
fn hover_function_return_annotation() {
    assert_hover!(
        "
/// Exciting documentation
/// Maybe even multiple lines
fn append(x: String, y: String) -> String {
  x <> y
}
",
        find_position_of("String").under_char('n').nth_occurrence(3)
    );
}

#[test]
fn hover_function_return_annotation_with_tuple() {
    assert_hover!(
        "
/// Exciting documentation
/// Maybe even multiple lines
fn append(x: String, y: String) -> #(String, String) {
  #(x, y)
}
",
        find_position_of("String").under_char('r').nth_occurrence(3)
    );
}

#[test]
fn hover_module_constant_annotation() {
    assert_hover!(
        "
/// Exciting documentation
/// Maybe even multiple lines
const one: Int = 1
",
        find_position_of("Int").under_last_char()
    );
}

#[test]
fn hover_type_constructor_annotation() {
    assert_hover!(
        "
type Wibble {
    Wibble(arg: String)
}
",
        find_position_of("String").under_char('n')
    );
}

#[test]
fn hover_type_alias_annotation() {
    assert_hover!("type Wibble = Int", find_position_of("Int").under_char('n'));
}

#[test]
fn hover_assignment_annotation() {
    assert_hover!(
        "
fn wibble() {
    let wobble: Int = 7
    wobble
}
",
        find_position_of("Int").under_last_char()
    );
}

#[test]
fn hover_function_arg_annotation_with_documentation() {
    assert_hover!(
        "
/// Exciting documentation
/// Maybe even multiple lines
type Wibble {
    Wibble(arg: String)
}

fn identity(x: Wibble) -> Wibble {
  x
}
",
        find_position_of("Wibble")
            .under_last_char()
            .nth_occurrence(3)
    );
}

#[test]
fn hover_import_unqualified_value() {
    let code = "
import example_module.{my_num}
fn main() {
  my_num
}
";

    assert_hover!(
        TestProject::for_source(code).add_module(
            "example_module",
            "
/// Exciting documentation
/// Maybe even multiple lines
pub const my_num = 1"
        ),
        find_position_of("my_num").under_char('n')
    );
}

#[test]
fn hover_import_unqualified_value_from_hex() {
    let code = "
import example_module.{my_num}
fn main() {
  my_num
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module(
            "example_module",
            "
/// Exciting documentation
/// Maybe even multiple lines
pub const my_num = 1"
        ),
        find_position_of("my_num").under_char('n')
    );
}

#[test]
fn hover_import_unqualified_type() {
    let code = "
import example_module.{type MyType, MyType}
fn main() -> MyType {
  MyType
}
";

    assert_hover!(
        TestProject::for_source(code).add_module(
            "example_module",
            "
/// Exciting documentation
/// Maybe even multiple lines
pub type MyType {
  MyType
}"
        ),
        find_position_of("MyType").under_last_char()
    );
}

#[test]
fn hover_works_even_for_invalid_code() {
    assert_hover!(
        "
fn invalid() { 1 + Nil }
fn valid() { Nil }
",
        find_position_of("fn valid").under_char('v')
    );
}

#[test]
fn hover_for_pattern_spread_ignoring_all_fields() {
    assert_hover!(
        "
pub type Model {
  Model(
    Int,
    Float,
    label1: Int,
    label2: String,
  )
}

pub fn main() {
  case todo {
    Model(..) -> todo
  }
}
",
        find_position_of("..")
    );
}

#[test]
fn hover_for_pattern_spread_ignoring_some_fields() {
    assert_hover!(
        "
pub type Model {
  Model(
    Int,
    Float,
    label1: Int,
    label2: String,
  )
}

pub fn main() {
  case todo {
    Model(_, label1: _, ..) -> todo
  }
}
",
        find_position_of("..").under_last_char()
    );
}

#[test]
fn hover_for_pattern_spread_ignoring_all_positional_fields() {
    assert_hover!(
        "
pub type Model {
  Model(
    Int,
    Float,
    label1: Int,
    label2: String,
  )
}

pub fn main() {
  case todo {
    Model(_, _, _, ..) -> todo
  }
}
",
        find_position_of("..")
    );
}

#[test]
fn hover_label_shorthand_in_call_arg() {
    assert_hover!(
        "
fn wibble(arg1 arg1: Int, arg2 arg2: Bool) { Nil }

fn main() {
  let arg1 = 1
  let arg2 = True
  wibble(arg2:, arg1:)
}
",
        find_position_of("arg2:").nth_occurrence(2)
    );
}

#[test]
fn hover_label_shorthand_in_pattern_call_arg() {
    assert_hover!(
        "
pub type Wibble { Wibble(arg1: Int, arg2: Bool) }

pub fn main() {
  case todo {
    Wibble(arg2:, ..) -> todo
  }
}
",
        find_position_of("arg2:")
            .nth_occurrence(2)
            .under_last_char()
    );
}

#[test]
fn hover_label_shorthand_in_pattern_call_arg_2() {
    assert_hover!(
        "
pub type Wibble { Wibble(arg1: Int, arg2: Bool) }

pub fn main() {
  let Wibble(arg2:, ..) = todo
}
",
        find_position_of("arg2:").nth_occurrence(2).under_char('r')
    );
}

#[test]
fn hover_contextual_type() {
    let code = "
import wibble/wobble
const value = wobble.Wobble
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wobble }"),
        find_position_of("value").under_char('v')
    );
}

#[test]
fn hover_contextual_type_aliased_module() {
    let code = "
import wibble/wobble as wubble
const value = wubble.Wobble
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wobble }"),
        find_position_of("value").under_char('v')
    );
}

#[test]
fn hover_contextual_type_unqualified() {
    let code = "
import wibble/wobble.{type Wibble}
const value = wobble.Wobble
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wobble }"),
        find_position_of("value").under_char('v')
    );
}

#[test]
fn hover_contextual_type_unqualified_aliased() {
    let code = "
import wibble/wobble.{type Wibble as Wobble}
const value = wobble.Wobble
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wobble }"),
        find_position_of("value").under_char('v')
    );
}

#[test]
fn hover_contextual_type_aliased() {
    let code = "
import wibble/wobble
type Local = wobble.Wibble
const value = wobble.Wobble
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wobble }"),
        find_position_of("value").under_char('v')
    );
}

#[test]
fn hover_contextual_type_function() {
    let code = "
import wibble/wobble
type MyInt = Int
fn func(value: wobble.Wibble) -> MyInt { 1 }
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wobble }"),
        find_position_of("func").under_char('f')
    );
}

#[test]
fn hover_contextual_type_unqualified_import() {
    let code = "
import wibble/wobble.{type Wibble as Wobble, Wobble}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wobble }"),
        find_position_of("Wobble}").under_char('W')
    );
}

#[test]
fn hover_contextual_type_pattern() {
    let code = "
import wibble/wobble.{Wibble, Wobble, Wubble}

pub fn cycle(wibble: wobble.Wibble) {
  case wibble {
    Wibble -> Wobble
    Wobble -> Wubble
    Wubble -> Wibble
  }
}
";

    assert_hover!(
        TestProject::for_source(code)
            .add_hex_module("wibble/wobble", "pub type Wibble { Wibble Wobble Wubble }"),
        find_position_of("Wubble ->").under_char('u')
    );
}

#[test]
fn hover_contextual_type_pattern_spread() {
    let code = "
import wibble/wobble.{type Wibble as Wobble}

type Thing {
  Thing(id: Int, value: Wobble)
}

pub fn main(thing: Thing) {
  case thing {
    Thing(id: 0, ..) -> 12
    _ -> 14
  }
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wibble }"),
        find_position_of("..").under_char('.')
    );
}

#[test]
fn hover_contextual_type_expression() {
    let code = "
import wibble/wobble

pub fn main() {
  let wibble = wobble.Wibble
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wibble }"),
        find_position_of(".Wibble").under_char('l')
    );
}

#[test]
fn hover_contextual_type_arg() {
    let code = "
import wibble/wobble

fn do_things(wibble: wobble.Wibble) { wibble }
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wibble }"),
        find_position_of("wibble:").under_char('w')
    );
}

#[test]
fn hover_print_type_variable_names() {
    let code = "
fn main(value: Result(ok, error)) {
  let v = value
  v
}
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("let v").under_char('v')
    );
}

#[test]
fn hover_print_unbound_type_variable_names() {
    let code = "
fn make_ok(value: some_type) {
  let result = Ok(value)
  result
}
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("result =").under_char('s')
    );
}

#[test]
fn hover_print_unbound_type_variable_name_without_conflicts() {
    let code = "
fn make_ok(value: a) {
  let result = Ok(value)
  result
}
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("result =").under_char('s')
    );
}

#[test]
fn hover_print_imported_alias() {
    let code = "
import aliases.{type Aliased}
const thing: Aliased = 10
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("aliases", "pub type Aliased = Int"),
        find_position_of("thing").under_char('g')
    );
}

#[test]
fn hover_prelude_type() {
    let code = "
const number = 100
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("number").under_char('b')
    );
}

#[test]
fn hover_shadowed_prelude_type() {
    let code = "
type Int { Int }
const number = 100
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("number").under_char('b')
    );
}

#[test]
fn hover_shadowed_prelude_type_imported() {
    let code = "
import numbers.{type Int}
const number = 100
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("numbers", "pub type Int"),
        find_position_of("number =").under_char('b')
    );
}

#[test]
fn hover_contextual_type_annotation() {
    let code = "
import wibble/wobble

fn make_wibble() -> wobble.Wibble { wobble.Wibble }
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wibble }"),
        find_position_of("-> wobble.Wibble").under_char('i')
    );
}

#[test]
fn hover_contextual_type_annotation_prelude() {
    let code = "
fn add_one(a: Int) -> Int {
  a + 1
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wibble }"),
        find_position_of("-> Int").under_char('I')
    );
}

#[test]
fn hover_contextual_type_annotation_unqualified() {
    let code = "
import wibble/wobble.{type Wibble}

fn main(wibble: Wibble) {
  wibble
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wibble }"),
        find_position_of(": Wibble").under_char('W')
    );
}

#[test]
fn hover_contextual_type_annotation_unqualified_aliased() {
    let code = "
import wibble/wobble.{type Wibble as Wubble}

fn main(wibble: Wubble) {
  wibble
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wibble }"),
        find_position_of(": Wubble").under_char('W')
    );
}

#[test]
fn hover_contextual_type_annotation_aliased_module() {
    let code = "
import wibble/wobble as wubble

fn main(wibble: wubble.Wibble) {
  wibble
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wibble }"),
        find_position_of(": wubble.Wibble").under_char('W')
    );
}

#[test]
fn hover_contextual_type_annotation_aliased() {
    let code = "
import wibble/wobble

type Wubble = wobble.Wibble

fn main(wibble: Wubble) {
  wibble
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("wibble/wobble", "pub type Wibble { Wibble }"),
        find_position_of(": Wubble").under_char('e')
    );
}

#[test]
fn hover_print_underlying_for_alias_with_parameters() {
    let code = "
type LocalResult = Result(String, Int)

fn do_thing() -> LocalResult {
  Error(1)
}
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("do_thing").under_char('d')
    );
}

#[test]
fn hover_print_alias_when_parameters_match() {
    let code = "
type MyResult(a, b) = Result(a, b)

fn do_thing() -> MyResult(Int, Int) {
  Error(1)
}
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("do_thing").under_char('d')
    );
}

#[test]
fn hover_print_underlying_for_imported_alias() {
    let code = "
import alias.{type A}

fn wibble() -> Result(Int, String) {
  todo
}
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("alias", "pub type A = Result(Int, String)"),
        find_position_of("wibble").under_char('l')
    );
}

#[test]
fn hover_print_aliased_imported_generic_type() {
    let code = "
import gleam/option.{type Option as Maybe}

const none: Maybe(Int) = option.None
";

    assert_hover!(
        TestProject::for_source(code)
            .add_hex_module("gleam/option", "pub type Option(a) { None Some(a) }"),
        find_position_of("none").under_char('e')
    );
}

#[test]
fn hover_print_qualified_prelude_type_when_shadowed_by_alias() {
    let code = "
type Result = #(Bool, String)
const ok = Ok(10)
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("ok").under_char('k')
    );
}

#[test]
fn hover_print_qualified_prelude_type_when_shadowed_by_imported_alias() {
    let code = "
import alias.{type Bool}
const value = True
";

    assert_hover!(
        TestProject::for_source(code).add_hex_module("alias", "pub type Bool = #(Int, Int)"),
        find_position_of("value").under_char('v')
    );
}

// https://github.com/gleam-lang/gleam/issues/3761
#[test]
fn hover_over_block_in_list_spread() {
    let code = "
pub fn main() {
  [1, 2, ..{
    let x = 1
    [x]
  }]
}
";

    assert_hover!(TestProject::for_source(code), find_position_of("x"));
}

// https://github.com/gleam-lang/gleam/issues/3758
#[test]
fn hover_for_anonymous_function_annotation() {
    let code = "
/// An example type.
pub type Wibble

pub fn main() {
  fn(w: Wibble) { todo }
}
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("w: Wibble").under_char('b')
    );
}

#[test]
fn hover_for_label_in_pattern() {
    let code = "
type Wibble {
  Wibble(wibble: Int, wobble: Int)
}

pub fn main() {
  let Wibble(wibble: _, wobble: _) = todo
  todo
}
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("wibble: _").under_char('l')
    );
}

#[test]
fn hover_for_label_in_expression() {
    let code = "
fn add(wibble a, wobble b) {
  a + b
}

pub fn main() {
  add(wibble: 1, wobble: 2)
}
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("wibble:").under_char('i')
    );
}

#[test]
fn hover_for_pattern_in_use() {
    let code = "
type Wibble {
  Wibble(Int, Float)
}

pub fn main() {
  use Wibble(int, float) <- todo
  todo
}
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("int").under_char('i')
    );
}

#[test]
fn hover_for_annotation_in_use() {
    let code = "
pub fn main() {
  use something: Int <- todo
  todo
}
";

    assert_hover!(
        TestProject::for_source(code),
        find_position_of("Int").under_char('n')
    );
}

#[test]
fn hover_on_pipe_with_invalid_step() {
    assert_hover!(
        "
pub fn main() {
  [1, 2, 3]
  |> map(wibble)
  |> filter(fn(value) { value })
}

fn map(list: List(a), fun: fn(a) -> b) -> List(b) {}
fn filter(list: List(a), fun: fn(a) -> Bool) -> List(a) {}
",
        find_position_of("[")
    );
}

#[test]
fn hover_on_pipe_with_invalid_step_1() {
    assert_hover!(
        "
pub fn main() {
  [1, 2, 3]
  |> map(wibble)
  |> filter(fn(value) { value })
}

fn map(list: List(a), fun: fn(a) -> b) -> List(b) {}
fn filter(list: List(a), fun: fn(a) -> Bool) -> List(a) {}
",
        find_position_of("1")
    );
}

#[test]
fn hover_on_pipe_with_invalid_step_2() {
    assert_hover!(
        "
pub fn main() {
  [1, 2, 3]
  |> map(wibble)
  |> filter(fn(value) { value })
}

fn map(list: List(a), fun: fn(a) -> b) -> List(b) {}
fn filter(list: List(a), fun: fn(a) -> Bool) -> List(a) {}
",
        find_position_of("map")
    );
}

#[test]
fn hover_on_pipe_with_invalid_step_3() {
    assert_hover!(
        "
pub fn main() {
  [1, 2, 3]
  |> map(wibble)
  |> filter(fn(value) { value })
}

fn map(list: List(a), fun: fn(a) -> b) -> List(b) {}
fn filter(list: List(a), fun: fn(a) -> Bool) -> List(a) {}
",
        find_position_of("wibble")
    );
}

#[test]
fn hover_on_pipe_with_invalid_step_4() {
    assert_hover!(
        "
pub fn main() {
  [1, 2, 3]
  |> map(wibble)
  |> filter(fn(value) { value })
}

fn map(list: List(a), fun: fn(a) -> b) -> List(b) {}
fn filter(list: List(a), fun: fn(a) -> Bool) -> List(a) {}
",
        find_position_of("filter")
    );
}

#[test]
fn hover_on_pipe_with_invalid_step_5() {
    assert_hover!(
        "
pub fn main() {
  [1, 2, 3]
  |> map(wibble)
  |> filter(fn(value) { value })
}

fn map(list: List(a), fun: fn(a) -> b) -> List(b) { todo }
fn filter(list: List(a), fun: fn(a) -> Bool) -> List(a) { todo }
",
        find_position_of("fn(value)")
    );
}

#[test]
fn hover_on_pipe_with_invalid_step_6() {
    assert_hover!(
        "
pub fn main() {
  [1, 2, 3]
  |> wibble
  |> filter(fn(value) { value })
}

fn filter(list: List(a), fun: fn(a) -> Bool) -> List(a) { todo }
",
        find_position_of("wibble")
    );
}

#[test]
fn hover_on_pipe_with_invalid_step_8() {
    assert_hover!(
        "
pub fn main() {
  [1, 2, 3]
  |> wibble
  |> filter(fn(value) { value })
}

fn filter(list: List(a), fun: fn(a) -> Bool) -> List(a) { todo }
",
        find_position_of("fn(value)")
    );
}

#[test]
fn hover_over_module_name() {
    let src = "
import wibble

pub fn main() {
  wibble.wibble()
}
";
    assert_hover!(
        TestProject::for_source(src).add_hex_module(
            "wibble",
            "
//// This is the wibble module.
//// Here is some documentation about it.
//// This module does stuff

pub fn wibble() {
  todo
}
"
        ),
        find_position_of("wibble.")
    );
}

#[test]
fn hover_over_module_with_path() {
    let src = "
import wibble/wobble

pub fn main() {
  wobble.wibble()
}
";
    assert_hover!(
        TestProject::for_source(src).add_hex_module(
            "wibble/wobble",
            "
//// The module documentation

pub fn wibble() {
  todo
}
"
        ),
        find_position_of("wobble.")
    );
}

#[test]
fn hover_over_module_name_in_annotation() {
    let src = "
import wibble

pub fn main(w: wibble.Wibble) {
  todo
}
";
    assert_hover!(
        TestProject::for_source(src).add_hex_module(
            "wibble",
            "
//// This is the wibble module.
//// Here is some documentation about it.
//// This module does stuff

pub type Wibble
"
        ),
        find_position_of("wibble.")
    );
}

#[test]
fn hover_over_imported_module() {
    let src = "
import wibble
";
    assert_hover!(
        TestProject::for_source(src).add_hex_module(
            "wibble",
            "
//// This is the wibble module.
//// Here is some documentation about it.
//// This module does stuff
"
        ),
        find_position_of("wibble")
    );
}

#[test]
fn no_hexdocs_link_when_hovering_over_local_module() {
    let src = "
import wibble
";
    assert_hover!(
        TestProject::for_source(src).add_module(
            "wibble",
            "
//// This is the wibble module.
//// Here is some documentation about it.
//// This module does stuff
"
        ),
        find_position_of("wibble")
    );
}

#[test]
fn hover_for_constant_int() {
    assert_hover!(
        "
const ten = 10
",
        find_position_of("10")
    );
}

#[test]
fn hover_for_constant_float() {
    assert_hover!(
        "
const pi = 3.14
",
        find_position_of("3.14")
    );
}
#[test]
fn hover_for_constant_string() {
    assert_hover!(
        r#"
const message = "Hello!"
"#,
        find_position_of("!")
    );
}

#[test]
fn hover_for_constant_other_constant() {
    assert_hover!(
        "
const constant1 = 10
const constant2 = constant1
",
        find_position_of("= constant1").under_char('s')
    );
}

#[test]
fn hover_for_constant_record() {
    assert_hover!(
        "
type Wibble {
  Wibble(Int)
}

const w = Wibble(10)
",
        find_position_of("Wibble(10)").under_char('i')
    );
}

#[test]
fn hover_for_constant_tuple() {
    assert_hover!(
        "
const tuple = #(1, 3.5, False)
",
        find_position_of("#(")
    );
}

#[test]
fn hover_for_constant_tuple_element() {
    assert_hover!(
        "
const tuple = #(1, 3.5, False)
",
        find_position_of("False")
    );
}

#[test]
fn hover_for_constant_list() {
    assert_hover!(
        "
const numbers = [2, 4, 6, 8]
",
        find_position_of("[")
    );
}

#[test]
fn hover_for_constant_list_element() {
    assert_hover!(
        "
const numbers = [2, 4, 6, 8]
",
        find_position_of("4")
    );
}

#[test]
fn hover_for_constant_string_concatenation() {
    assert_hover!(
        r#"
const name = "Bob"
const message = "Hello " <> name
"#,
        find_position_of("<>")
    );
}

#[test]
fn hover_for_constant_string_concatenation_side() {
    assert_hover!(
        r#"
const name = "Bob"
const message = "Hello " <> name
"#,
        find_position_of("<> name").under_char('n')
    );
}

#[test]
fn hover_for_constant_bit_array() {
    assert_hover!(
        "
const bits = <<1:2, 3:4>>
",
        find_position_of(",")
    );
}

#[test]
fn hover_for_constant_bit_array_segment() {
    assert_hover!(
        "
const bits = <<1:2, 3:4>>
",
        find_position_of("1")
    );
}

#[test]
fn hover_for_constant_bit_array_segment_option() {
    assert_hover!(
        "
const bits = <<1:size(2), 3:4>>
",
        find_position_of("2")
    );
}

#[test]
fn hover_for_nested_constant() {
    assert_hover!(
        "
type Wibble {
  Wibble
  Wobble(BitArray)
}

const value = #(1, 2, [Wibble, Wobble(<<1, 2, 3>>), Wibble])
",
        find_position_of("3")
    );
}

#[test]
fn record_field_documentation() {
    assert_hover!(
        "
pub type Wibble {
  Wibble(
    /// This is some documentation about the wibble field.
    wibble: Int
  )
}

pub fn wibble(w: Wibble) {
  w.wibble
}
",
        find_position_of("w.wibble").under_char('l')
    );
}

#[test]
fn no_documentation_for_shared_record_field() {
    assert_hover!(
        "
pub type Wibble {
  Wibble(
    /// This is some documentation about the wibble field.
    wibble: Int
  )
  Wobble(
    /// Here's some documentation explaining a field of Wobble
    wibble: Int
  )
}

pub fn wibble(w: Wibble) {
  w.wibble
}
",
        find_position_of("w.wibble").under_char('l')
    );
}

#[test]
fn documentation_for_shared_record_field_when_variant_is_known() {
    assert_hover!(
        "
pub type Wibble {
  Wibble(
    /// This is some documentation about the wibble field.
    wibble: Int
  )
  Wobble(
    /// This won't show up because it's a Wibble variant
    wibble: Int
  )
}

pub fn wibble(w: Wibble) {
  let assert Wibble(..) = w
  w.wibble
}
",
        find_position_of("w.wibble").under_char('l')
    );
}

#[test]
fn hover_for_string_prefix_pattern() {
    assert_hover!(
        "
pub fn main() {
  case \"wibble\" {
    \"wib\" as alias <> suffix -> alias <> suffix
    other -> other
  }
}
",
        find_position_of("<>")
    );
}

#[test]
fn hover_for_string_prefix_pattern_prefix_alias() {
    assert_hover!(
        "
pub fn main() {
  case \"wibble\" {
    \"wib\" as alias <> suffix -> alias <> suffix
    other -> other
  }
}
",
        find_position_of("alias").nth_occurrence(1)
    );
}

#[test]
fn hover_for_string_prefix_pattern_suffix_variable() {
    assert_hover!(
        "
pub fn main() {
  case \"wibble\" {
    \"wib\" as alias <> suffix -> alias <> suffix
    other -> other
  }
}
",
        find_position_of("suffix").nth_occurrence(1)
    );
}

#[test]
fn hover_for_string_prefix_pattern_prefix_alias_alternative_definition() {
    assert_hover!(
        "
pub fn main() {
  case \"wibble\" {
    \"wib\" as prefix <> rest | \"wob\" as prefix <> rest -> prefix <> rest
    other -> other
  }
}
",
        find_position_of("prefix").nth_occurrence(2)
    );
}

#[test]
fn hover_for_string_prefix_pattern_suffix_variable_alternative_definition() {
    assert_hover!(
        "
pub fn main() {
  case \"wibble\" {
    \"wib\" <> rest | \"wob\" <> rest -> rest
    other -> other
  }
}
",
        find_position_of("rest").nth_occurrence(2)
    );
}

#[test]
fn hover_for_string_prefix_pattern_suffix_variable_discard() {
    assert_hover!(
        "
pub fn main() {
  case \"wibble\" {
    \"wib\" <> _discard -> Nil
    other -> Nil
  }
}
",
        find_position_of("_discard")
    );
}

#[test]
fn hover_for_custom_type() {
    assert_hover!(
        "/// Exciting documentation
/// Maybe even multiple lines
type Wibble {
  /// Some more exciting documentation
  Wibble(String)
  /// The most exciting documentation
  Wobble(arg: Int)
}",
        find_position_of("Wibble")
    );
}

#[test]
fn hover_type_constructor_with_no_fields() {
    assert_hover!(
        "
/// Exciting documentation
/// Maybe even multiple lines
type Wibble {
    /// Some more exciting documentation
    Wibble
}
",
        find_position_of("Wibble").nth_occurrence(2)
    );
}

#[test]
fn hover_type_constructor_with_label() {
    assert_hover!(
        "
/// Exciting documentation
/// Maybe even multiple lines
type Wibble {
    /// Some more exciting documentation
    Wibble(arg: String)
    /// The most exciting documentation
    Wobble(Int)
}
",
        find_position_of("Wibble").nth_occurrence(2)
    );
}

#[test]
fn hover_type_constructor_with_no_label() {
    assert_hover!(
        "
/// Exciting documentation
/// Maybe even multiple lines
type Wibble {
    /// Some more exciting documentation
    Wibble(arg: String)
    /// The most exciting documentation
    Wobble(Int)
}
",
        find_position_of("Wobble")
    );
}

#[test]
fn hover_for_local_variable_from_guard() {
    assert_hover!(
        "
pub fn main() {
  let wibble = True
  let wobble = False
  case wibble {
    True if wobble -> !wibble
    False if !wobble -> wibble
    _ -> wobble
  }
}
",
        find_position_of("wobble").nth_occurrence(2).under_char('o')
    );
}

#[test]
fn hover_for_record_from_guard() {
    assert_hover!(
        "
type Wibble {
  Wibble
  Wobble
}

pub fn main() {
  let wibble = True
  let wobble = Wibble
  case wibble {
    True if wobble == Wibble -> !wibble
    False if wobble == Wobble -> wibble
    _ -> Wibble
  }
}
",
        find_position_of("== Wibble").under_char('l')
    );
}

#[test]
fn hover_for_module_select_from_guard() {
    let src = "
import mod

pub fn main() {
  let wibble = True
  case wibble {
    True if mod.wibble < 5 -> !wibble
    False if mod.wibble != 10 -> wibble
    _ -> mod.wibble + 1
  }
}
";
    assert_hover!(
        TestProject::for_source(src).add_module("mod", "pub const wibble = 10"),
        find_position_of("mod.wibble").under_char('w')
    );
}

#[test]
fn hover_for_record_module_select_from_guard() {
    let src = "
import mod

pub fn main() {
  let wibble = True
  let wobble = mod.Wibble
  case wibble {
    True if wobble == mod.Wobble -> !wibble
    False if wobble == mod.Wibble -> wibble
    _ -> mod.Wibble
  }
}
";
    assert_hover!(
        TestProject::for_source(src).add_module("mod", "pub type Wobble { Wibble Wobble }"),
        find_position_of("== mod.Wibble").under_char('i')
    );
}

#[test]
fn hover_for_module_select_pattern() {
    let src = "
import mod
pub fn go(x: mod.Wibble) {
  case x {
    mod.Wibble -> 1
  }
}
";
    assert_hover!(
        TestProject::for_source(src).add_module("mod", "pub type Wibble { Wibble }"),
        find_position_of("mod.Wibble")
            .under_char('W')
            .nth_occurrence(2)
    );
}

#[test]
fn hover_on_record_dot_of_record_update_shows_ignored_fields() {
    assert_hover!(
        "
type Wibble {
  Wibble(a: Int, b: Bool)
}

pub fn go(wibble: Wibble) {
    Wibble(..wibble, a: 1)
}
",
        find_position_of("..")
    );
}

#[test]
fn hover_on_record_being_updated_shows_ignored_fields() {
    assert_hover!(
        "
type Wibble {
  Wibble(a: Int, b: Bool)
}

pub fn go(wibble: Wibble) {
    Wibble(..wibble, a: 1)
}
",
        find_position_of("..wibble").under_last_char()
    );
}

#[test]
fn hover_for_invalid_record_update_1() {
    assert_hover!(
        "
type Wibble {
  Wibble(a: Int, b: Bool)
}

pub fn go(wibble: Wibble) {
    Wibble(..wibble, a: True, b: 1)
}
",
        find_position_of("True")
    );
}

#[test]
fn hover_for_invalid_record_update_2() {
    assert_hover!(
        "
type Wibble {
  Wibble(a: Int, b: Bool)
}

pub fn go(wibble: Wibble) {
    Wibble(..wibble, a: True, b: 1)
}
",
        find_position_of("1")
    );
}

#[test]
fn hover_for_invalid_record_update_3() {
    assert_hover!(
        "
type Wibble {
  Wibble(a: Int, b: Bool)
}

pub fn go(wibble: Wibble) {
    Wibble(..wibble, a: True, b: 1)
}
",
        find_position_of("Wibble").nth_occurrence(4)
    );
}

#[test]
fn hover_on_constant_todo() {
    assert_hover!(
        "
const wibble = todo
",
        find_position_of("wibble")
    );
}

#[test]
fn hover_on_constant_todo_1() {
    assert_hover!(
        "
const wibble = todo
",
        find_position_of("todo")
    );
}

#[test]
fn hover_on_constant_todo_in_list() {
    assert_hover!(
        "
const wibble = [todo, 1]
",
        find_position_of("wibble")
    );
}

#[test]
fn hover_on_constant_todo_in_list_2() {
    assert_hover!(
        "
const wibble = [todo, 1]
",
        find_position_of("todo")
    );
}

#[test]
fn hover_on_constant_todo_in_bit_array() {
    assert_hover!(
        "
const wibble = <<todo:utf8>>
",
        find_position_of("todo")
    );
}

#[test]
fn hover_on_constant_todo_in_bit_array_2() {
    assert_hover!(
        "
const wibble = <<todo>>
",
        find_position_of("todo")
    );
}

#[test]
fn hover_on_constant_todo_in_constructor() {
    assert_hover!(
        "
const wibble = Ok(todo)
",
        find_position_of("wibble")
    );
}

#[test]
fn hover_on_constant_todo_in_annotated_constructor() {
    assert_hover!(
        "
const wibble: Result(String, Int) = Ok(todo)
",
        find_position_of("todo")
    );
}

#[test]
fn hover_on_constant_todo_in_constructor_2() {
    assert_hover!(
        "
const wibble = Ok(todo)
",
        find_position_of("todo")
    );
}

#[test]
fn hover_on_constant_annotated_todo() {
    assert_hover!(
        "
const wibble: String = todo
",
        find_position_of("todo")
    );
}

#[test]
fn hover_on_constant_todo_message() {
    assert_hover!(
        r#"
const wibble = todo as "wobble"
"#,
        find_position_of("wobble")
    );
}

#[test]
fn hover_on_constant_todo_invalid_message_still_works() {
    assert_hover!(
        r#"
const wobble = 1
const wibble = todo as [wobble]
"#,
        find_position_of("wobble").nth_occurrence(2)
    );
}
