#![allow(rustc::symbol_intern_string_literal)]
use std::assert_matches::assert_matches;
use std::io::prelude::*;
use std::iter::Peekable;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::{io, str};

use ast::token::IdentIsRaw;
use rustc_ast::token::{self, Delimiter, Token};
use rustc_ast::tokenstream::{DelimSpacing, DelimSpan, Spacing, TokenStream, TokenTree};
use rustc_ast::{self as ast, PatKind, visit};
use rustc_ast_pretty::pprust::item_to_string;
use rustc_errors::annotate_snippet_emitter_writer::AnnotateSnippetEmitter;
use rustc_errors::emitter::{HumanEmitter, OutputTheme};
use rustc_errors::translation::Translator;
use rustc_errors::{AutoStream, DiagCtxt, MultiSpan, PResult};
use rustc_session::parse::ParseSess;
use rustc_span::source_map::{FilePathMapping, SourceMap};
use rustc_span::{
    BytePos, FileName, Pos, Span, Symbol, create_default_session_globals_then, kw, sym,
};

use crate::lexer::StripTokens;
use crate::parser::{ForceCollect, Parser};
use crate::{new_parser_from_source_str, source_str_to_stream, unwrap_or_emit_fatal};

fn psess() -> ParseSess {
    ParseSess::new(vec![crate::DEFAULT_LOCALE_RESOURCE])
}

fn filename(sm: &SourceMap, path: &str) -> FileName {
    FileName::Real(sm.path_mapping().to_real_filename(sm.working_dir(), PathBuf::from(path)))
}

/// Map string to parser (via tts).
fn string_to_parser(psess: &ParseSess, source_str: String) -> Parser<'_> {
    unwrap_or_emit_fatal(new_parser_from_source_str(
        psess,
        filename(psess.source_map(), "bogofile"),
        source_str,
        StripTokens::Nothing,
    ))
}

fn create_test_handler(theme: OutputTheme) -> (DiagCtxt, Arc<SourceMap>, Arc<Mutex<Vec<u8>>>) {
    let output = Arc::new(Mutex::new(Vec::new()));
    let source_map = Arc::new(SourceMap::new(FilePathMapping::empty()));
    let translator = Translator::with_fallback_bundle(vec![crate::DEFAULT_LOCALE_RESOURCE], false);
    let shared: Box<dyn Write + Send> = Box::new(Shared { data: output.clone() });
    let auto_stream = AutoStream::never(shared);
    let dcx = DiagCtxt::new(match theme {
        OutputTheme::Ascii => Box::new(
            HumanEmitter::new(auto_stream, translator)
                .sm(Some(source_map.clone()))
                .diagnostic_width(Some(140))
                .theme(theme),
        ),
        OutputTheme::Unicode => Box::new(
            AnnotateSnippetEmitter::new(auto_stream, translator)
                .sm(Some(source_map.clone()))
                .diagnostic_width(Some(140))
                .theme(theme),
        ),
    });
    (dcx, source_map, output)
}

/// Returns the result of parsing the given string via the given callback.
///
/// If there are any errors, this will panic.
fn with_error_checking_parse<'a, T, F>(s: String, psess: &'a ParseSess, f: F) -> T
where
    F: FnOnce(&mut Parser<'a>) -> PResult<'a, T>,
{
    let mut p = string_to_parser(&psess, s);
    let x = f(&mut p).unwrap();
    p.dcx().abort_if_errors();
    x
}

/// Verifies that parsing the given string using the given callback will
/// generate an error that contains the given text.
fn with_expected_parse_error<T, F>(source_str: &str, expected_output: &str, f: F)
where
    F: for<'a> FnOnce(&mut Parser<'a>) -> PResult<'a, T>,
{
    let (handler, source_map, output) = create_test_handler(OutputTheme::Ascii);
    let psess = ParseSess::with_dcx(handler, source_map);
    let mut p = string_to_parser(&psess, source_str.to_string());
    let result = f(&mut p);
    assert!(result.is_ok());

    let bytes = output.lock().unwrap();
    let actual_output = str::from_utf8(&bytes).unwrap();
    println!("expected output:\n------\n{}------", expected_output);
    println!("actual output:\n------\n{}------", actual_output);

    assert!(actual_output.contains(expected_output))
}

/// Maps a string to tts, using a made-up filename.
pub(crate) fn string_to_stream(source_str: String) -> TokenStream {
    let psess = psess();
    unwrap_or_emit_fatal(source_str_to_stream(
        &psess,
        filename(psess.source_map(), "bogofile"),
        source_str,
        None,
    ))
}

/// Does the given string match the pattern? whitespace in the first string
/// may be deleted or replaced with other whitespace to match the pattern.
/// This function is relatively Unicode-ignorant; fortunately, the careful design
/// of UTF-8 mitigates this ignorance. It doesn't do NKF-normalization(?).
pub(crate) fn matches_codepattern(a: &str, b: &str) -> bool {
    let mut a_iter = a.chars().peekable();
    let mut b_iter = b.chars().peekable();

    loop {
        let (a, b) = match (a_iter.peek(), b_iter.peek()) {
            (None, None) => return true,
            (None, _) => return false,
            (Some(&a), None) => {
                if rustc_lexer::is_whitespace(a) {
                    break; // Trailing whitespace check is out of loop for borrowck.
                } else {
                    return false;
                }
            }
            (Some(&a), Some(&b)) => (a, b),
        };

        if rustc_lexer::is_whitespace(a) && rustc_lexer::is_whitespace(b) {
            // Skip whitespace for `a` and `b`.
            scan_for_non_ws_or_end(&mut a_iter);
            scan_for_non_ws_or_end(&mut b_iter);
        } else if rustc_lexer::is_whitespace(a) {
            // Skip whitespace for `a`.
            scan_for_non_ws_or_end(&mut a_iter);
        } else if a == b {
            a_iter.next();
            b_iter.next();
        } else {
            return false;
        }
    }

    // Check if a has *only* trailing whitespace.
    a_iter.all(rustc_lexer::is_whitespace)
}

/// Advances the given peekable `Iterator` until it reaches a non-whitespace character.
fn scan_for_non_ws_or_end<I: Iterator<Item = char>>(iter: &mut Peekable<I>) {
    while iter.peek().copied().is_some_and(rustc_lexer::is_whitespace) {
        iter.next();
    }
}

/// Identifies a position in the text by the n'th occurrence of a string.
struct Position {
    string: &'static str,
    count: usize,
}

struct SpanLabel {
    start: Position,
    end: Position,
    label: &'static str,
}

struct Shared<T: Write> {
    data: Arc<Mutex<T>>,
}

impl<T: Write> Write for Shared<T> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.data.lock().unwrap().write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.data.lock().unwrap().flush()
    }
}

#[allow(rustc::untranslatable_diagnostic)] // no translation needed for tests
fn test_harness(
    file_text: &str,
    span_labels: Vec<SpanLabel>,
    notes: Vec<(Option<(Position, Position)>, &'static str)>,
    expected_output_ascii: &str,
    expected_output_unicode: &str,
) {
    create_default_session_globals_then(|| {
        for (theme, expected_output) in [
            (OutputTheme::Ascii, expected_output_ascii),
            (OutputTheme::Unicode, expected_output_unicode),
        ] {
            let (dcx, source_map, output) = create_test_handler(theme);
            source_map.new_source_file(filename(&source_map, "test.rs"), file_text.to_owned());

            let primary_span = make_span(&file_text, &span_labels[0].start, &span_labels[0].end);
            let mut msp = MultiSpan::from_span(primary_span);
            for span_label in &span_labels {
                let span = make_span(&file_text, &span_label.start, &span_label.end);
                msp.push_span_label(span, span_label.label);
                println!("span: {:?} label: {:?}", span, span_label.label);
                println!("text: {:?}", source_map.span_to_snippet(span));
            }

            let mut err = dcx.handle().struct_span_err(msp, "foo");
            for (position, note) in &notes {
                if let Some((start, end)) = position {
                    let span = make_span(&file_text, &start, &end);
                    err.span_note(span, *note);
                } else {
                    err.note(*note);
                }
            }
            err.emit();

            assert!(
                expected_output.chars().next() == Some('\n'),
                "expected output should begin with newline"
            );
            let expected_output = &expected_output[1..];

            let bytes = output.lock().unwrap();
            let actual_output = str::from_utf8(&bytes).unwrap();
            println!("expected output:\n------\n{}------", expected_output);
            println!("actual output:\n------\n{}------", actual_output);

            assert!(expected_output == actual_output)
        }
    })
}

fn make_span(file_text: &str, start: &Position, end: &Position) -> Span {
    let start = make_pos(file_text, start);
    let end = make_pos(file_text, end) + end.string.len(); // just after matching thing ends
    assert!(start <= end);
    Span::with_root_ctxt(BytePos(start as u32), BytePos(end as u32))
}

fn make_pos(file_text: &str, pos: &Position) -> usize {
    let mut remainder = file_text;
    let mut offset = 0;
    for _ in 0..pos.count {
        if let Some(n) = remainder.find(&pos.string) {
            offset += n;
            remainder = &remainder[n + 1..];
        } else {
            panic!("failed to find {} instances of {:?} in {:?}", pos.count, pos.string, file_text);
        }
    }
    offset
}

#[test]
fn ends_on_col0() {
    test_harness(
        r#"
fn foo() {
}
"#,
        vec![SpanLabel {
            start: Position { string: "{", count: 1 },
            end: Position { string: "}", count: 1 },
            label: "test",
        }],
        vec![],
        r#"
error: foo
 --> test.rs:2:10
  |
2 |   fn foo() {
  |  __________^
3 | | }
  | |_^ test

"#,
        r#"
error: foo
  ╭▸ test.rs:2:10
  │
2 │   fn foo() {
  │ ┏━━━━━━━━━━┛
3 │ ┃ }
  ╰╴┗━┛ test

"#,
    );
}

#[test]
fn ends_on_col2() {
    test_harness(
        r#"
fn foo() {


  }
"#,
        vec![SpanLabel {
            start: Position { string: "{", count: 1 },
            end: Position { string: "}", count: 1 },
            label: "test",
        }],
        vec![],
        r#"
error: foo
 --> test.rs:2:10
  |
2 |   fn foo() {
  |  __________^
... |
5 | |   }
  | |___^ test

"#,
        r#"
error: foo
  ╭▸ test.rs:2:10
  │
2 │   fn foo() {
  │ ┏━━━━━━━━━━┛
  ‡ ┃
5 │ ┃   }
  ╰╴┗━━━┛ test

"#,
    );
}
#[test]
fn non_nested() {
    test_harness(
        r#"
fn foo() {
  X0 Y0
  X1 Y1
  X2 Y2
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "Y2", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |      X0 Y0
  |  ____^  -
  | | ______|
4 | ||   X1 Y1
5 | ||   X2 Y2
  | ||____^__- `Y` is a good letter too
  | |_____|
  |       `X` is a good letter

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │      X0 Y0
  │ ┏━━━━┛  │
  │ ┃┌──────┘
4 │ ┃│   X1 Y1
5 │ ┃│   X2 Y2
  │ ┃└────╿──┘ `Y` is a good letter too
  │ ┗━━━━━┥
  ╰╴      `X` is a good letter

"#,
    );
}

#[test]
fn nested() {
    test_harness(
        r#"
fn foo() {
  X0 Y0
  Y1 X1
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X1", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "Y1", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |      X0 Y0
  |  ____^  -
  | | ______|
4 | ||   Y1 X1
  | ||____-__^ `X` is a good letter
  |  |____|
  |       `Y` is a good letter too

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │      X0 Y0
  │ ┏━━━━┛  │
  │ ┃┌──────┘
4 │ ┃│   Y1 X1
  │ ┗│━━━━│━━┛ `X` is a good letter
  │  └────┤
  ╰╴      `Y` is a good letter too

"#,
    );
}

#[test]
fn multiline_and_normal_overlap() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "Y0", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |     X0 Y0 Z0
  |  ___---^-
  | |   |
  | |   `Y` is a good letter too
4 | |   X1 Y1 Z1
5 | |   X2 Y2 Z2
  | |____^ `X` is a good letter

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │     X0 Y0 Z0
  │ ┏━━━┬──┛─
  │ ┃   │
  │ ┃   `Y` is a good letter too
4 │ ┃   X1 Y1 Z1
5 │ ┃   X2 Y2 Z2
  ╰╴┗━━━━┛ `X` is a good letter

"#,
    );
}

#[test]
fn different_overlap() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Z1", count: 1 },
                end: Position { string: "X3", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |      X0 Y0 Z0
  |  _______^
4 | |    X1 Y1 Z1
  | | _________-
5 | ||   X2 Y2 Z2
  | ||____^ `X` is a good letter
6 |  |   X3 Y3 Z3
  |  |____- `Y` is a good letter too

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │      X0 Y0 Z0
  │ ┏━━━━━━━┛
4 │ ┃    X1 Y1 Z1
  │ ┃┌─────────┘
5 │ ┃│   X2 Y2 Z2
  │ ┗│━━━━┛ `X` is a good letter
6 │  │   X3 Y3 Z3
  ╰╴ └────┘ `Y` is a good letter too

"#,
    );
}

#[test]
fn different_note_1() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![(None, "bar")],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
  = note: bar

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  │
  ╰ note: bar

"#,
    );
}

#[test]
fn different_note_2() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![(None, "bar"), (None, "qux")],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
  = note: bar
  = note: qux

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  │
  ├ note: bar
  ╰ note: qux

"#,
    );
}

#[test]
fn different_note_3() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![(None, "bar"), (None, "baz"), (None, "qux")],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
  = note: bar
  = note: baz
  = note: qux

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  │
  ├ note: bar
  ├ note: baz
  ╰ note: qux

"#,
    );
}

#[test]
fn different_note_spanned_1() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![(
            Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
            "bar",
        )],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
note: bar
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  ╰╴
note: bar
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━

"#,
    );
}

#[test]
fn different_note_spanned_2() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "bar",
            ),
            (
                Some((Position { string: "X2", count: 1 }, Position { string: "Y2", count: 1 })),
                "qux",
            ),
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
note: bar
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^
note: qux
 --> test.rs:5:3
  |
5 |   X2 Y2 Z2
  |   ^^^^^

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  ╰╴
note: bar
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━
note: qux
  ╭▸ test.rs:5:3
  │
5 │   X2 Y2 Z2
  ╰╴  ━━━━━

"#,
    );
}

#[test]
fn different_note_spanned_3() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "bar",
            ),
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "baz",
            ),
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "qux",
            ),
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
note: bar
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^
note: baz
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^
note: qux
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  ╰╴
note: bar
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━
note: baz
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━
note: qux
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━

"#,
    );
}

#[test]
fn different_note_spanned_4() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "bar",
            ),
            (None, "qux"),
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
note: bar
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^
  = note: qux

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  ╰╴
note: bar
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  │   ━━━━━━━━
  ╰ note: qux

"#,
    );
}

#[test]
fn different_note_spanned_5() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![
            (None, "bar"),
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "qux",
            ),
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
  = note: bar
note: qux
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  │
  ╰ note: bar
note: qux
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━

"#,
    );
}

#[test]
fn different_note_spanned_6() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![
            (None, "bar"),
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "baz",
            ),
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "qux",
            ),
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
  = note: bar
note: baz
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^
note: qux
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  │
  ╰ note: bar
note: baz
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━
note: qux
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━

"#,
    );
}

#[test]
fn different_note_spanned_7() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z3", count: 1 })),
                "bar",
            ),
            (None, "baz"),
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "qux",
            ),
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
note: bar
 --> test.rs:4:3
  |
4 | /   X1 Y1 Z1
5 | |   X2 Y2 Z2
6 | |   X3 Y3 Z3
  | |__________^
  = note: baz
note: qux
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  ╰╴
note: bar
  ╭▸ test.rs:4:3
  │
4 │ ┏   X1 Y1 Z1
5 │ ┃   X2 Y2 Z2
6 │ ┃   X3 Y3 Z3
  │ ┗━━━━━━━━━━┛
  ╰ note: baz
note: qux
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━

"#,
    );
}

#[test]
fn different_note_spanned_8() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "bar",
            ),
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "baz",
            ),
            (None, "qux"),
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
note: bar
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^
note: baz
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^
  = note: qux

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  ╰╴
note: bar
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━
note: baz
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  │   ━━━━━━━━
  ╰ note: qux

"#,
    );
}

#[test]
fn different_note_spanned_9() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![
            (None, "bar"),
            (None, "baz"),
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "qux",
            ),
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
  = note: bar
  = note: baz
note: qux
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  │
  ├ note: bar
  ╰ note: baz
note: qux
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  ╰╴  ━━━━━━━━

"#,
    );
}

#[test]
fn different_note_spanned_10() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![SpanLabel {
            start: Position { string: "Y0", count: 1 },
            end: Position { string: "Z0", count: 1 },
            label: "`X` is a good letter",
        }],
        vec![
            (
                Some((Position { string: "X1", count: 1 }, Position { string: "Z1", count: 1 })),
                "bar",
            ),
            (None, "baz"),
            (None, "qux"),
        ],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |   X0 Y0 Z0
  |      ^^^^^ `X` is a good letter
  |
note: bar
 --> test.rs:4:3
  |
4 |   X1 Y1 Z1
  |   ^^^^^^^^
  = note: baz
  = note: qux

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │   X0 Y0 Z0
  │      ━━━━━ `X` is a good letter
  ╰╴
note: bar
  ╭▸ test.rs:4:3
  │
4 │   X1 Y1 Z1
  │   ━━━━━━━━
  ├ note: baz
  ╰ note: qux

"#,
    );
}

#[test]
fn triple_overlap() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "Y2", count: 1 },
                label: "`Y` is a good letter too",
            },
            SpanLabel {
                start: Position { string: "Z0", count: 1 },
                end: Position { string: "Z2", count: 1 },
                label: "`Z` label",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |       X0 Y0 Z0
  |  _____^  -  -
  | | _______|  |
  | || _________|
4 | |||   X1 Y1 Z1
5 | |||   X2 Y2 Z2
  | |||____^__-__- `Z` label
  | ||_____|__|
  | |______|  `Y` is a good letter too
  |        `X` is a good letter

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │       X0 Y0 Z0
  │ ┏━━━━━┛  │  │
  │ ┃┌───────┘  │
  │ ┃│┌─────────┘
4 │ ┃││   X1 Y1 Z1
5 │ ┃││   X2 Y2 Z2
  │ ┃│└────╿──│──┘ `Z` label
  │ ┃└─────│──┤
  │ ┗━━━━━━┥  `Y` is a good letter too
  ╰╴       `X` is a good letter

"#,
    );
}

#[test]
fn triple_exact_overlap() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`Y` is a good letter too",
            },
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X2", count: 1 },
                label: "`Z` label",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 | /   X0 Y0 Z0
4 | |   X1 Y1 Z1
5 | |   X2 Y2 Z2
  | |    ^
  | |    |
  | |    `X` is a good letter
  | |____`Y` is a good letter too
  |      `Z` label

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │ ┏   X0 Y0 Z0
4 │ ┃   X1 Y1 Z1
5 │ ┃   X2 Y2 Z2
  │ ┃    ╿
  │ ┃    │
  │ ┃    `X` is a good letter
  │ ┗━━━━`Y` is a good letter too
  ╰╴     `Z` label

"#,
    );
}

#[test]
fn minimum_depth() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "X1", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Y1", count: 1 },
                end: Position { string: "Z2", count: 1 },
                label: "`Y` is a good letter too",
            },
            SpanLabel {
                start: Position { string: "X2", count: 1 },
                end: Position { string: "Y3", count: 1 },
                label: "`Z`",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |      X0 Y0 Z0
  |  _______^
4 | |    X1 Y1 Z1
  | | ____^_-
  | ||____|
  |  |    `X` is a good letter
5 |  |   X2 Y2 Z2
  |  |___-______- `Y` is a good letter too
  |   ___|
  |  |
6 |  |   X3 Y3 Z3
  |  |_______- `Z`

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │      X0 Y0 Z0
  │ ┏━━━━━━━┛
4 │ ┃    X1 Y1 Z1
  │ ┃┌────╿─┘
  │ ┗│━━━━┥
  │  │    `X` is a good letter
5 │  │   X2 Y2 Z2
  │  └───│──────┘ `Y` is a good letter too
  │  ┌───┘
  │  │
6 │  │   X3 Y3 Z3
  ╰╴ └───────┘ `Z`

"#,
    );
}

#[test]
fn non_overlapping() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "X0", count: 1 },
                end: Position { string: "X1", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Y2", count: 1 },
                end: Position { string: "Z3", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 | /   X0 Y0 Z0
4 | |   X1 Y1 Z1
  | |____^ `X` is a good letter
5 |     X2 Y2 Z2
  |  ______-
6 | |   X3 Y3 Z3
  | |__________- `Y` is a good letter too

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │ ┏   X0 Y0 Z0
4 │ ┃   X1 Y1 Z1
  │ ┗━━━━┛ `X` is a good letter
5 │     X2 Y2 Z2
  │ ┌──────┘
6 │ │   X3 Y3 Z3
  ╰╴└──────────┘ `Y` is a good letter too

"#,
    );
}

#[test]
fn overlapping_start_and_end() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "X1", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Z1", count: 1 },
                end: Position { string: "Z3", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:6
  |
3 |      X0 Y0 Z0
  |  _______^
4 | |    X1 Y1 Z1
  | | ____^____-
  | ||____|
  |  |    `X` is a good letter
5 |  |   X2 Y2 Z2
6 |  |   X3 Y3 Z3
  |  |__________- `Y` is a good letter too

"#,
        r#"
error: foo
  ╭▸ test.rs:3:6
  │
3 │      X0 Y0 Z0
  │ ┏━━━━━━━┛
4 │ ┃    X1 Y1 Z1
  │ ┃┌────╿────┘
  │ ┗│━━━━┥
  │  │    `X` is a good letter
5 │  │   X2 Y2 Z2
6 │  │   X3 Y3 Z3
  ╰╴ └──────────┘ `Y` is a good letter too

"#,
    );
}

#[test]
fn multiple_labels_primary_without_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "`a` is a good letter",
            },
            SpanLabel {
                start: Position { string: "c", count: 1 },
                end: Position { string: "c", count: 1 },
                label: "",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:7
  |
3 |   a { b { c } d }
  |   ----^^^^-^^-- `a` is a good letter

"#,
        r#"
error: foo
  ╭▸ test.rs:3:7
  │
3 │   a { b { c } d }
  ╰╴  ────━━━━─━━── `a` is a good letter

"#,
    );
}

#[test]
fn multiline_notes() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![SpanLabel {
            start: Position { string: "a", count: 1 },
            end: Position { string: "d", count: 1 },
            label: "`a` is a good letter",
        }],
        vec![(None, "foo\nbar"), (None, "foo\nbar")],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^^^^^^^^^^ `a` is a good letter
  |
  = note: foo
          bar
  = note: foo
          bar

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │   a { b { c } d }
  │   ━━━━━━━━━━━━━ `a` is a good letter
  │
  ├ note: foo
  │       bar
  ╰ note: foo
          bar

"#,
    );
}

#[test]
fn multiple_labels_secondary_without_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "`a` is a good letter",
            },
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^-------^^ `a` is a good letter

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │   a { b { c } d }
  ╰╴  ━━━━───────━━ `a` is a good letter

"#,
    );
}

#[test]
fn multiple_labels_primary_without_message_2() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "`b` is a good letter",
            },
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "c", count: 1 },
                end: Position { string: "c", count: 1 },
                label: "",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:7
  |
3 |   a { b { c } d }
  |   ----^^^^-^^--
  |       |
  |       `b` is a good letter

"#,
        r#"
error: foo
  ╭▸ test.rs:3:7
  │
3 │   a { b { c } d }
  │   ────┯━━━─━━──
  │       │
  ╰╴      `b` is a good letter

"#,
    );
}

#[test]
fn multiple_labels_secondary_without_message_2() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "`b` is a good letter",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^-------^^
  |       |
  |       `b` is a good letter

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │   a { b { c } d }
  │   ━━━━┬──────━━
  │       │
  ╰╴      `b` is a good letter

"#,
    );
}

#[test]
fn multiple_labels_secondary_without_message_3() {
    test_harness(
        r#"
fn foo() {
  a  bc  d
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "b", count: 1 },
                label: "`a` is a good letter",
            },
            SpanLabel {
                start: Position { string: "c", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a  bc  d
  |   ^^^^----
  |   |
  |   `a` is a good letter

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │   a  bc  d
  │   ┯━━━────
  │   │
  ╰╴  `a` is a good letter

"#,
    );
}

#[test]
fn multiple_labels_without_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^-------^^

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │   a { b { c } d }
  ╰╴  ━━━━───────━━

"#,
    );
}

#[test]
fn multiple_labels_without_message_2() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "",
            },
            SpanLabel {
                start: Position { string: "c", count: 1 },
                end: Position { string: "c", count: 1 },
                label: "",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:7
  |
3 |   a { b { c } d }
  |   ----^^^^-^^--

"#,
        r#"
error: foo
  ╭▸ test.rs:3:7
  │
3 │   a { b { c } d }
  ╰╴  ────━━━━─━━──

"#,
    );
}

#[test]
fn multiple_labels_with_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "a", count: 1 },
                end: Position { string: "d", count: 1 },
                label: "`a` is a good letter",
            },
            SpanLabel {
                start: Position { string: "b", count: 1 },
                end: Position { string: "}", count: 1 },
                label: "`b` is a good letter",
            },
        ],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^-------^^
  |   |   |
  |   |   `b` is a good letter
  |   `a` is a good letter

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │   a { b { c } d }
  │   ┯━━━┬──────━━
  │   │   │
  │   │   `b` is a good letter
  ╰╴  `a` is a good letter

"#,
    );
}

#[test]
fn single_label_with_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![SpanLabel {
            start: Position { string: "a", count: 1 },
            end: Position { string: "d", count: 1 },
            label: "`a` is a good letter",
        }],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^^^^^^^^^^ `a` is a good letter

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │   a { b { c } d }
  ╰╴  ━━━━━━━━━━━━━ `a` is a good letter

"#,
    );
}

#[test]
fn single_label_without_message() {
    test_harness(
        r#"
fn foo() {
  a { b { c } d }
}
"#,
        vec![SpanLabel {
            start: Position { string: "a", count: 1 },
            end: Position { string: "d", count: 1 },
            label: "",
        }],
        vec![],
        r#"
error: foo
 --> test.rs:3:3
  |
3 |   a { b { c } d }
  |   ^^^^^^^^^^^^^

"#,
        r#"
error: foo
  ╭▸ test.rs:3:3
  │
3 │   a { b { c } d }
  ╰╴  ━━━━━━━━━━━━━

"#,
    );
}

#[test]
fn long_snippet() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
  X1 Y1 Z1
1
2
3
4
5
6
7
8
9
10
  X2 Y2 Z2
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "X1", count: 1 },
                label: "`X` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Z1", count: 1 },
                end: Position { string: "Z3", count: 1 },
                label: "`Y` is a good letter too",
            },
        ],
        vec![],
        r#"
error: foo
  --> test.rs:3:6
   |
 3 |      X0 Y0 Z0
   |  _______^
 4 | |    X1 Y1 Z1
   | | ____^____-
   | ||____|
   |  |    `X` is a good letter
 5 |  | 1
 6 |  | 2
 7 |  | 3
...   |
15 |  |   X2 Y2 Z2
16 |  |   X3 Y3 Z3
   |  |__________- `Y` is a good letter too

"#,
        r#"
error: foo
   ╭▸ test.rs:3:6
   │
 3 │      X0 Y0 Z0
   │ ┏━━━━━━━┛
 4 │ ┃    X1 Y1 Z1
   │ ┃┌────╿────┘
   │ ┗│━━━━┥
   │  │    `X` is a good letter
 5 │  │ 1
 6 │  │ 2
 7 │  │ 3
   ‡  │
15 │  │   X2 Y2 Z2
16 │  │   X3 Y3 Z3
   ╰╴ └──────────┘ `Y` is a good letter too

"#,
    );
}

#[test]
fn long_snippet_multiple_spans() {
    test_harness(
        r#"
fn foo() {
  X0 Y0 Z0
1
2
3
  X1 Y1 Z1
4
5
6
  X2 Y2 Z2
7
8
9
10
  X3 Y3 Z3
}
"#,
        vec![
            SpanLabel {
                start: Position { string: "Y0", count: 1 },
                end: Position { string: "Y3", count: 1 },
                label: "`Y` is a good letter",
            },
            SpanLabel {
                start: Position { string: "Z1", count: 1 },
                end: Position { string: "Z2", count: 1 },
                label: "`Z` is a good letter too",
            },
        ],
        vec![],
        r#"
error: foo
  --> test.rs:3:6
   |
 3 |      X0 Y0 Z0
   |  _______^
 4 | |  1
 5 | |  2
 6 | |  3
 7 | |    X1 Y1 Z1
   | | _________-
 8 | || 4
 9 | || 5
10 | || 6
11 | ||   X2 Y2 Z2
   | ||__________- `Z` is a good letter too
...  |
15 | |  10
16 | |    X3 Y3 Z3
   | |________^ `Y` is a good letter

"#,
        r#"
error: foo
   ╭▸ test.rs:3:6
   │
 3 │      X0 Y0 Z0
   │ ┏━━━━━━━┛
 4 │ ┃  1
 5 │ ┃  2
 6 │ ┃  3
 7 │ ┃    X1 Y1 Z1
   │ ┃┌─────────┘
 8 │ ┃│ 4
 9 │ ┃│ 5
10 │ ┃│ 6
11 │ ┃│   X2 Y2 Z2
   │ ┃└──────────┘ `Z` is a good letter too
   ‡ ┃
15 │ ┃  10
16 │ ┃    X3 Y3 Z3
   ╰╴┗━━━━━━━━┛ `Y` is a good letter

"#,
    );
}

/// Parses an item.
///
/// Returns `Ok(Some(item))` when successful, `Ok(None)` when no item was found, and `Err`
/// when a syntax error occurred.
fn parse_item_from_source_str(
    name: FileName,
    source: String,
    psess: &ParseSess,
) -> PResult<'_, Option<Box<ast::Item>>> {
    unwrap_or_emit_fatal(new_parser_from_source_str(psess, name, source, StripTokens::Nothing))
        .parse_item(ForceCollect::No)
}

// Produces a `rustc_span::span`.
fn sp(a: u32, b: u32) -> Span {
    Span::with_root_ctxt(BytePos(a), BytePos(b))
}

/// Parses a string, return an expression.
fn string_to_expr(source_str: String) -> Box<ast::Expr> {
    with_error_checking_parse(source_str, &psess(), |p| p.parse_expr())
}

/// Parses a string, returns an item.
fn string_to_item(source_str: String) -> Option<Box<ast::Item>> {
    with_error_checking_parse(source_str, &psess(), |p| p.parse_item(ForceCollect::No))
}

#[test]
fn bad_path_expr_1() {
    // This should trigger error: expected identifier, found keyword `return`
    create_default_session_globals_then(|| {
        with_expected_parse_error(
            "::abc::def::return",
            "expected identifier, found keyword `return`",
            |p| p.parse_expr(),
        );
    })
}

// Checks the token-tree-ization of macros.
#[test]
fn string_to_tts_macro() {
    create_default_session_globals_then(|| {
        let stream = string_to_stream("macro_rules! zip (($a)=>($a))".to_string());
        let tts = &stream.iter().collect::<Vec<_>>()[..];

        match tts {
            [
                TokenTree::Token(
                    Token { kind: token::Ident(name_macro_rules, IdentIsRaw::No), .. },
                    _,
                ),
                TokenTree::Token(Token { kind: token::Bang, .. }, _),
                TokenTree::Token(Token { kind: token::Ident(name_zip, IdentIsRaw::No), .. }, _),
                TokenTree::Delimited(.., macro_delim, macro_tts),
            ] if name_macro_rules == &kw::MacroRules && name_zip.as_str() == "zip" => {
                let tts = &macro_tts.iter().collect::<Vec<_>>();
                match &tts[..] {
                    [
                        TokenTree::Delimited(.., first_delim, first_tts),
                        TokenTree::Token(Token { kind: token::FatArrow, .. }, _),
                        TokenTree::Delimited(.., second_delim, second_tts),
                    ] if macro_delim == &Delimiter::Parenthesis => {
                        let tts = &first_tts.iter().collect::<Vec<_>>();
                        match &tts[..] {
                            [
                                TokenTree::Token(Token { kind: token::Dollar, .. }, _),
                                TokenTree::Token(
                                    Token { kind: token::Ident(name, IdentIsRaw::No), .. },
                                    _,
                                ),
                            ] if first_delim == &Delimiter::Parenthesis && name.as_str() == "a" => {
                            }
                            _ => panic!("value 3: {:?} {:?}", first_delim, first_tts),
                        }
                        let tts = &second_tts.iter().collect::<Vec<_>>();
                        match &tts[..] {
                            [
                                TokenTree::Token(Token { kind: token::Dollar, .. }, _),
                                TokenTree::Token(
                                    Token { kind: token::Ident(name, IdentIsRaw::No), .. },
                                    _,
                                ),
                            ] if second_delim == &Delimiter::Parenthesis
                                && name.as_str() == "a" => {}
                            _ => panic!("value 4: {:?} {:?}", second_delim, second_tts),
                        }
                    }
                    _ => panic!("value 2: {:?} {:?}", macro_delim, macro_tts),
                }
            }
            _ => panic!("value: {:?}", tts),
        }
    })
}

#[test]
fn string_to_tts_1() {
    create_default_session_globals_then(|| {
        let tts = string_to_stream("fn a(b: i32) { b; }".to_string());

        let expected = TokenStream::new(vec![
            TokenTree::token_alone(token::Ident(kw::Fn, IdentIsRaw::No), sp(0, 2)),
            TokenTree::token_joint_hidden(
                token::Ident(Symbol::intern("a"), IdentIsRaw::No),
                sp(3, 4),
            ),
            TokenTree::Delimited(
                DelimSpan::from_pair(sp(4, 5), sp(11, 12)),
                // `JointHidden` because the `(` is followed immediately by
                // `b`, `Alone` because the `)` is followed by whitespace.
                DelimSpacing::new(Spacing::JointHidden, Spacing::Alone),
                Delimiter::Parenthesis,
                TokenStream::new(vec![
                    TokenTree::token_joint(
                        token::Ident(Symbol::intern("b"), IdentIsRaw::No),
                        sp(5, 6),
                    ),
                    TokenTree::token_alone(token::Colon, sp(6, 7)),
                    // `JointHidden` because the `i32` is immediately followed by the `)`.
                    TokenTree::token_joint_hidden(
                        token::Ident(sym::i32, IdentIsRaw::No),
                        sp(8, 11),
                    ),
                ]),
            ),
            TokenTree::Delimited(
                DelimSpan::from_pair(sp(13, 14), sp(18, 19)),
                // First `Alone` because the `{` is followed by whitespace,
                // second `Alone` because the `}` is followed immediately by
                // EOF.
                DelimSpacing::new(Spacing::Alone, Spacing::Alone),
                Delimiter::Brace,
                TokenStream::new(vec![
                    TokenTree::token_joint(
                        token::Ident(Symbol::intern("b"), IdentIsRaw::No),
                        sp(15, 16),
                    ),
                    // `Alone` because the `;` is followed by whitespace.
                    TokenTree::token_alone(token::Semi, sp(16, 17)),
                ]),
            ),
        ]);

        assert_eq!(tts, expected);
    })
}

#[test]
fn parse_use() {
    create_default_session_globals_then(|| {
        let use_s = "use foo::bar::baz;";
        let vitem = string_to_item(use_s.to_string()).unwrap();
        let vitem_s = item_to_string(&vitem);
        assert_eq!(&vitem_s[..], use_s);

        let use_s = "use foo::bar as baz;";
        let vitem = string_to_item(use_s.to_string()).unwrap();
        let vitem_s = item_to_string(&vitem);
        assert_eq!(&vitem_s[..], use_s);
    })
}

#[test]
fn parse_extern_crate() {
    create_default_session_globals_then(|| {
        let ex_s = "extern crate foo;";
        let vitem = string_to_item(ex_s.to_string()).unwrap();
        let vitem_s = item_to_string(&vitem);
        assert_eq!(&vitem_s[..], ex_s);

        let ex_s = "extern crate foo as bar;";
        let vitem = string_to_item(ex_s.to_string()).unwrap();
        let vitem_s = item_to_string(&vitem);
        assert_eq!(&vitem_s[..], ex_s);
    })
}

fn get_spans_of_pat_idents(src: &str) -> Vec<Span> {
    let item = string_to_item(src.to_string()).unwrap();

    struct PatIdentVisitor {
        spans: Vec<Span>,
    }
    impl<'a> visit::Visitor<'a> for PatIdentVisitor {
        fn visit_pat(&mut self, p: &'a ast::Pat) {
            match &p.kind {
                PatKind::Ident(_, ident, _) => {
                    self.spans.push(ident.span);
                }
                _ => {
                    visit::walk_pat(self, p);
                }
            }
        }
    }
    let mut v = PatIdentVisitor { spans: Vec::new() };
    visit::walk_item(&mut v, &item);
    return v.spans;
}

#[test]
fn span_of_self_arg_pat_idents_are_correct() {
    create_default_session_globals_then(|| {
        let srcs = [
            "impl z { fn a (&self, &myarg: i32) {} }",
            "impl z { fn a (&mut self, &myarg: i32) {} }",
            "impl z { fn a (&'a self, &myarg: i32) {} }",
            "impl z { fn a (self, &myarg: i32) {} }",
            "impl z { fn a (self: Foo, &myarg: i32) {} }",
        ];

        for src in srcs {
            let spans = get_spans_of_pat_idents(src);
            let (lo, hi) = (spans[0].lo(), spans[0].hi());
            assert!(
                "self" == &src[lo.to_usize()..hi.to_usize()],
                "\"{}\" != \"self\". src=\"{}\"",
                &src[lo.to_usize()..hi.to_usize()],
                src
            )
        }
    })
}

#[test]
fn parse_exprs() {
    create_default_session_globals_then(|| {
        // just make sure that they parse....
        string_to_expr("3 + 4".to_string());
        string_to_expr("a::z.froob(b,&(987+3))".to_string());
    })
}

#[test]
fn attrs_fix_bug() {
    create_default_session_globals_then(|| {
        string_to_item(
            "pub fn mk_file_writer(path: &Path, flags: &[FileFlag])
                -> Result<Box<Writer>, String> {
#[cfg(windows)]
fn wb() -> c_int {
    (O_WRONLY | libc::consts::os::extra::O_BINARY) as c_int
}

#[cfg(unix)]
fn wb() -> c_int { O_WRONLY as c_int }

let mut fflags: c_int = wb();
}"
            .to_string(),
        );
    })
}

#[test]
fn crlf_doc_comments() {
    create_default_session_globals_then(|| {
        let psess = psess();

        let name_1 = FileName::Custom("crlf_source_1".to_string());
        let source = "/// doc comment\r\nfn foo() {}".to_string();
        let item = parse_item_from_source_str(name_1, source, &psess).unwrap().unwrap();
        let doc = item.attrs.iter().filter_map(|at| at.doc_str()).next().unwrap();
        assert_eq!(doc.as_str(), " doc comment");

        let name_2 = FileName::Custom("crlf_source_2".to_string());
        let source = "/// doc comment\r\n/// line 2\r\nfn foo() {}".to_string();
        let item = parse_item_from_source_str(name_2, source, &psess).unwrap().unwrap();
        let docs = item.attrs.iter().filter_map(|at| at.doc_str()).collect::<Vec<_>>();
        let b: &[_] = &[Symbol::intern(" doc comment"), Symbol::intern(" line 2")];
        assert_eq!(&docs[..], b);

        let name_3 = FileName::Custom("clrf_source_3".to_string());
        let source = "/** doc comment\r\n *  with CRLF */\r\nfn foo() {}".to_string();
        let item = parse_item_from_source_str(name_3, source, &psess).unwrap().unwrap();
        let doc = item.attrs.iter().filter_map(|at| at.doc_str()).next().unwrap();
        assert_eq!(doc.as_str(), " doc comment\n *  with CRLF ");
    });
}

#[test]
fn ttdelim_span() {
    fn parse_expr_from_source_str(
        name: FileName,
        source: String,
        psess: &ParseSess,
    ) -> PResult<'_, Box<ast::Expr>> {
        unwrap_or_emit_fatal(new_parser_from_source_str(psess, name, source, StripTokens::Nothing))
            .parse_expr()
    }

    create_default_session_globals_then(|| {
        let psess = psess();
        let expr = parse_expr_from_source_str(
            filename(psess.source_map(), "foo"),
            "foo!( fn main() { body } )".to_string(),
            &psess,
        )
        .unwrap();

        let ast::ExprKind::MacCall(mac) = &expr.kind else { panic!("not a macro") };
        let span = mac.args.tokens.iter().last().unwrap().span();

        match psess.source_map().span_to_snippet(span) {
            Ok(s) => assert_eq!(&s[..], "{ body }"),
            Err(_) => panic!("could not get snippet"),
        }
    });
}

#[track_caller]
fn look(p: &Parser<'_>, dist: usize, kind: rustc_ast::token::TokenKind) {
    // Do the `assert_eq` outside the closure so that `track_caller` works.
    // (`#![feature(closure_track_caller)]` + `#[track_caller]` on the closure
    // doesn't give the line number in the test below if the assertion fails.)
    let tok = p.look_ahead(dist, |tok| *tok);
    assert_eq!(kind, tok.kind);
}

#[test]
fn look_ahead() {
    create_default_session_globals_then(|| {
        let sym_f = Symbol::intern("f");
        let sym_x = Symbol::intern("x");
        #[allow(non_snake_case)]
        let sym_S = Symbol::intern("S");
        let raw_no = IdentIsRaw::No;

        let psess = psess();
        let mut p = string_to_parser(&psess, "fn f(x: u32) { x } struct S;".to_string());

        // Current position is the `fn`.
        look(&p, 0, token::Ident(kw::Fn, raw_no));
        look(&p, 1, token::Ident(sym_f, raw_no));
        look(&p, 2, token::OpenParen);
        look(&p, 3, token::Ident(sym_x, raw_no));
        look(&p, 4, token::Colon);
        look(&p, 5, token::Ident(sym::u32, raw_no));
        look(&p, 6, token::CloseParen);
        look(&p, 7, token::OpenBrace);
        look(&p, 8, token::Ident(sym_x, raw_no));
        look(&p, 9, token::CloseBrace);
        look(&p, 10, token::Ident(kw::Struct, raw_no));
        look(&p, 11, token::Ident(sym_S, raw_no));
        look(&p, 12, token::Semi);
        // Any lookahead past the end of the token stream returns `Eof`.
        look(&p, 13, token::Eof);
        look(&p, 14, token::Eof);
        look(&p, 15, token::Eof);
        look(&p, 100, token::Eof);

        // Move forward to the first `x`.
        for _ in 0..3 {
            p.bump();
        }
        look(&p, 0, token::Ident(sym_x, raw_no));
        look(&p, 1, token::Colon);
        look(&p, 2, token::Ident(sym::u32, raw_no));
        look(&p, 3, token::CloseParen);
        look(&p, 4, token::OpenBrace);
        look(&p, 5, token::Ident(sym_x, raw_no));
        look(&p, 6, token::CloseBrace);
        look(&p, 7, token::Ident(kw::Struct, raw_no));
        look(&p, 8, token::Ident(sym_S, raw_no));
        look(&p, 9, token::Semi);
        look(&p, 10, token::Eof);
        look(&p, 11, token::Eof);
        look(&p, 100, token::Eof);

        // Move forward to the `;`.
        for _ in 0..9 {
            p.bump();
        }
        look(&p, 0, token::Semi);
        // Any lookahead past the end of the token stream returns `Eof`.
        look(&p, 1, token::Eof);
        look(&p, 100, token::Eof);

        // Move one past the `;`, i.e. past the end of the token stream.
        p.bump();
        look(&p, 0, token::Eof);
        look(&p, 1, token::Eof);
        look(&p, 100, token::Eof);

        // Bumping after Eof is idempotent.
        p.bump();
        look(&p, 0, token::Eof);
        look(&p, 1, token::Eof);
        look(&p, 100, token::Eof);
    });
}

/// There used to be some buggy behaviour when using `look_ahead` not within
/// the outermost token stream, which this test covers.
#[test]
fn look_ahead_non_outermost_stream() {
    create_default_session_globals_then(|| {
        let sym_f = Symbol::intern("f");
        let sym_x = Symbol::intern("x");
        #[allow(non_snake_case)]
        let sym_S = Symbol::intern("S");
        let raw_no = IdentIsRaw::No;

        let psess = psess();
        let mut p = string_to_parser(&psess, "mod m { fn f(x: u32) { x } struct S; }".to_string());

        // Move forward to the `fn`, which is not within the outermost token
        // stream (because it's inside the `mod { ... }`).
        for _ in 0..3 {
            p.bump();
        }
        look(&p, 0, token::Ident(kw::Fn, raw_no));
        look(&p, 1, token::Ident(sym_f, raw_no));
        look(&p, 2, token::OpenParen);
        look(&p, 3, token::Ident(sym_x, raw_no));
        look(&p, 4, token::Colon);
        look(&p, 5, token::Ident(sym::u32, raw_no));
        look(&p, 6, token::CloseParen);
        look(&p, 7, token::OpenBrace);
        look(&p, 8, token::Ident(sym_x, raw_no));
        look(&p, 9, token::CloseBrace);
        look(&p, 10, token::Ident(kw::Struct, raw_no));
        look(&p, 11, token::Ident(sym_S, raw_no));
        look(&p, 12, token::Semi);
        look(&p, 13, token::CloseBrace);
        // Any lookahead past the end of the token stream returns `Eof`.
        look(&p, 14, token::Eof);
        look(&p, 15, token::Eof);
        look(&p, 100, token::Eof);
    });
}

#[test]
fn debug_lookahead() {
    create_default_session_globals_then(|| {
        let psess = psess();
        let mut p = string_to_parser(&psess, "fn f(x: u32) { x } struct S;".to_string());

        // Current position is the `fn`.
        assert_eq!(
            &format!("{:#?}", p.debug_lookahead(0)),
            "Parser {
    prev_token: Token {
        kind: Question,
        span: Span {
            lo: BytePos(
                0,
            ),
            hi: BytePos(
                0,
            ),
            ctxt: #0,
        },
    },
    tokens: [],
    approx_token_stream_pos: 0,
    ..
}"
        );
        assert_eq!(
            &format!("{:#?}", p.debug_lookahead(7)),
            "Parser {
    prev_token: Token {
        kind: Question,
        span: Span {
            lo: BytePos(
                0,
            ),
            hi: BytePos(
                0,
            ),
            ctxt: #0,
        },
    },
    tokens: [
        Ident(
            \"fn\",
            No,
        ),
        Ident(
            \"f\",
            No,
        ),
        OpenParen,
        Ident(
            \"x\",
            No,
        ),
        Colon,
        Ident(
            \"u32\",
            No,
        ),
        CloseParen,
    ],
    approx_token_stream_pos: 0,
    ..
}"
        );
        // There are 13 tokens. We request 15, get 14; the last one is `Eof`.
        assert_eq!(
            &format!("{:#?}", p.debug_lookahead(15)),
            "Parser {
    prev_token: Token {
        kind: Question,
        span: Span {
            lo: BytePos(
                0,
            ),
            hi: BytePos(
                0,
            ),
            ctxt: #0,
        },
    },
    tokens: [
        Ident(
            \"fn\",
            No,
        ),
        Ident(
            \"f\",
            No,
        ),
        OpenParen,
        Ident(
            \"x\",
            No,
        ),
        Colon,
        Ident(
            \"u32\",
            No,
        ),
        CloseParen,
        OpenBrace,
        Ident(
            \"x\",
            No,
        ),
        CloseBrace,
        Ident(
            \"struct\",
            No,
        ),
        Ident(
            \"S\",
            No,
        ),
        Semi,
        Eof,
    ],
    approx_token_stream_pos: 0,
    ..
}"
        );

        // Move forward to the second `x`.
        for _ in 0..8 {
            p.bump();
        }
        assert_eq!(
            &format!("{:#?}", p.debug_lookahead(1)),
            "Parser {
    prev_token: Token {
        kind: OpenBrace,
        span: Span {
            lo: BytePos(
                13,
            ),
            hi: BytePos(
                14,
            ),
            ctxt: #0,
        },
    },
    tokens: [
        Ident(
            \"x\",
            No,
        ),
    ],
    approx_token_stream_pos: 8,
    ..
}"
        );
        assert_eq!(
            &format!("{:#?}", p.debug_lookahead(4)),
            "Parser {
    prev_token: Token {
        kind: OpenBrace,
        span: Span {
            lo: BytePos(
                13,
            ),
            hi: BytePos(
                14,
            ),
            ctxt: #0,
        },
    },
    tokens: [
        Ident(
            \"x\",
            No,
        ),
        CloseBrace,
        Ident(
            \"struct\",
            No,
        ),
        Ident(
            \"S\",
            No,
        ),
    ],
    approx_token_stream_pos: 8,
    ..
}"
        );

        // Move two past the final token (the `;`).
        for _ in 0..6 {
            p.bump();
        }
        assert_eq!(
            &format!("{:#?}", p.debug_lookahead(3)),
            "Parser {
    prev_token: Token {
        kind: Eof,
        span: Span {
            lo: BytePos(
                27,
            ),
            hi: BytePos(
                28,
            ),
            ctxt: #0,
        },
    },
    tokens: [
        Eof,
    ],
    approx_token_stream_pos: 14,
    ..
}"
        );
    });
}

// This tests that when parsing a string (rather than a file) we don't try
// and read in a file for a module declaration and just parse a stub.
// See `recurse_into_file_modules` in the parser.
#[test]
fn out_of_line_mod() {
    create_default_session_globals_then(|| {
        let psess = psess();
        let item = parse_item_from_source_str(
            filename(psess.source_map(), "foo"),
            "mod foo { struct S; mod this_does_not_exist; }".to_owned(),
            &psess,
        )
        .unwrap()
        .unwrap();

        let ast::ItemKind::Mod(_, _, mod_kind) = &item.kind else { panic!() };
        assert_matches!(mod_kind, ast::ModKind::Loaded(items, ..) if items.len() == 2);
    });
}

#[test]
fn eqmodws() {
    assert_eq!(matches_codepattern("", ""), true);
    assert_eq!(matches_codepattern("", "a"), false);
    assert_eq!(matches_codepattern("a", ""), false);
    assert_eq!(matches_codepattern("a", "a"), true);
    assert_eq!(matches_codepattern("a b", "a   \n\t\r  b"), true);
    assert_eq!(matches_codepattern("a b ", "a   \n\t\r  b"), true);
    assert_eq!(matches_codepattern("a b", "a   \n\t\r  b "), false);
    assert_eq!(matches_codepattern("a   b", "a b"), true);
    assert_eq!(matches_codepattern("ab", "a b"), false);
    assert_eq!(matches_codepattern("a   b", "ab"), true);
    assert_eq!(matches_codepattern(" a   b", "ab"), true);
}

#[test]
fn pattern_whitespace() {
    assert_eq!(matches_codepattern("", "\x0C"), false);
    assert_eq!(matches_codepattern("a b ", "a   \u{0085}\n\t\r  b"), true);
    assert_eq!(matches_codepattern("a b", "a   \u{0085}\n\t\r  b "), false);
}

#[test]
fn non_pattern_whitespace() {
    // These have the property 'White_Space' but not 'Pattern_White_Space'
    assert_eq!(matches_codepattern("a b", "a\u{2002}b"), false);
    assert_eq!(matches_codepattern("a   b", "a\u{2002}b"), false);
    assert_eq!(matches_codepattern("\u{205F}a   b", "ab"), false);
    assert_eq!(matches_codepattern("a  \u{3000}b", "ab"), false);
}
