#![cfg(feature = "integration-tests")] // TODO: switch to NaiveParser API
use std::ops::Range;

use codespan_reporting::{
    self,
    diagnostic::{Diagnostic, Label},
    files::SimpleFiles,
    term::{self, termcolor::Buffer},
};
use html5tokenizer::{offset::PosTrackingReader, DefaultEmitter, Token, Tokenizer};
use insta::assert_snapshot;
use similar_asserts::assert_eq;

fn tokenizer(html: &'static str) -> impl Iterator<Item = Token<usize>> {
    Tokenizer::new(
        PosTrackingReader::new(html),
        DefaultEmitter::<usize>::default(),
    )
    .flatten()
}

fn annotate(html: &str, labels: Vec<(Range<usize>, impl AsRef<str>)>) -> String {
    let mut files = SimpleFiles::new();
    let file_id = files.add("test.html", html);

    let diagnostic = Diagnostic::note().with_labels(
        labels
            .into_iter()
            .map(|(span, text)| Label::primary(file_id, span).with_message(text.as_ref()))
            .collect(),
    );

    let mut writer = Buffer::no_color();
    let config = codespan_reporting::term::Config::default();
    term::emit(&mut writer, &config, &files, &diagnostic).unwrap();
    let msg = std::str::from_utf8(writer.as_slice()).unwrap();

    // strip the filename and the line numbers since we don't need them
    // (apparently they cannot be disabled in codespan_reporting)
    msg.lines()
        .skip(3)
        .flat_map(|l| l.split_once("│ ").map(|s| s.1.trim_end()))
        .collect::<Vec<_>>()
        .join("\n")
}

#[test]
fn start_tag_span() {
    let html = "<x> <xyz> <xyz  > <xyz/>";
    let mut labels = Vec::new();
    for token in tokenizer(html) {
        if let Token::StartTag(tag) = token {
            labels.push((tag.span, ""));
        }
    }
    assert_snapshot!(annotate(html, labels), @r###"
    <x> <xyz> <xyz  > <xyz/>
    ^^^ ^^^^^ ^^^^^^^ ^^^^^^
    "###);
}

#[test]
fn end_tag_span() {
    let html = "</x> </xyz> </xyz  > </xyz/>";
    let mut labels = Vec::new();
    for token in tokenizer(html) {
        if let Token::EndTag(tag) = token {
            labels.push((tag.span, ""));
        }
    }
    assert_snapshot!(annotate(html, labels), @r###"
    </x> </xyz> </xyz  > </xyz/>
    ^^^^ ^^^^^^ ^^^^^^^^ ^^^^^^^
    "###);
}

#[test]
fn start_tag_name_span() {
    let html = "<x> <xyz> <xyz  > <xyz/>";
    let mut labels = Vec::new();
    for token in tokenizer(html) {
        if let Token::StartTag(tag) = token {
            labels.push((tag.name_span(), ""));
        }
    }
    assert_snapshot!(annotate(html, labels), @r###"
    <x> <xyz> <xyz  > <xyz/>
     ^   ^^^   ^^^     ^^^
    "###);
}

#[test]
fn end_tag_name_span() {
    let html = "</x> </xyz> </xyz  > </xyz/>";
    let mut labels = Vec::new();
    for token in tokenizer(html) {
        if let Token::EndTag(tag) = token {
            labels.push((tag.name_span(), ""));
        }
    }
    assert_snapshot!(annotate(html, labels), @r###"
    </x> </xyz> </xyz  > </xyz/>
      ^    ^^^    ^^^      ^^^
    "###);
}

#[test]
fn attribute_name_span() {
    let html = "<test x xyz y=VAL xy=VAL z = VAL yzx = VAL>";
    let mut labels = Vec::new();
    let Token::StartTag(tag) = tokenizer(html).next().unwrap() else {
        panic!("expected start tag")
    };
    for attr in &tag.attributes {
        labels.push((attr.name_span(), ""));
    }
    assert_snapshot!(annotate(html, labels), @r###"
    <test x xyz y=VAL xy=VAL z = VAL yzx = VAL>
          ^ ^^^ ^     ^^     ^       ^^^
    "###);
}

#[test]
fn attribute_value_span() {
    let html = "<test x=unquoted y = unquoted z='single-quoted' zz=\"double-quoted\" empty=''>";
    let mut labels = Vec::new();
    let Token::StartTag(tag) = tokenizer(html).next().unwrap() else {
        panic!("expected start tag")
    };
    for attr in &tag.attributes {
        labels.push((attr.value_span().unwrap(), ""));
    }
    assert_snapshot!(annotate(html, labels), @r###"
    <test x=unquoted y = unquoted z='single-quoted' zz="double-quoted" empty=''>
            ^^^^^^^^     ^^^^^^^^    ^^^^^^^^^^^^^      ^^^^^^^^^^^^^         ^
    "###);
}

#[test]
fn comment_proper_data_span() {
    let html = "<!-- Why are you looking at the source code? -->";
    let Token::Comment(comment) = tokenizer(html).next().unwrap() else {
        panic!("expected comment");
    };
    assert_eq!(comment.data, html[comment.data_span()]);
    let labels = vec![(comment.data_span(), "")];
    assert_snapshot!(annotate(html, labels), @r###"
    <!-- Why are you looking at the source code? -->
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    "###);
}

#[test]
fn comment_bogus_data_span() {
    let html = "<! Why are you looking at the source code? -->";
    let Token::Comment(comment) = tokenizer(html)
        .filter(|t| !matches!(t, Token::Error { .. }))
        .next()
        .unwrap()
    else {
        panic!("expected comment");
    };
    assert_eq!(comment.data, html[comment.data_span()]);
    let labels = vec![(comment.data_span(), "")];
    assert_snapshot!(annotate(html, labels), @r###"
    <! Why are you looking at the source code? -->
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    "###);
}

#[test]
fn doctype_span() {
    let html = r#"<!DOCTYPE       HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"     >"#;
    let Token::Doctype(doctype) = tokenizer(html).next().unwrap() else {
        panic!("expected doctype");
    };
    let labels = vec![(doctype.span, "")];
    assert_snapshot!(annotate(html, labels), @r###"
    <!DOCTYPE       HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"     >
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    "###);
}

#[test]
fn doctype_id_spans() {
    let html = r#"<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">"#;
    let Token::Doctype(doctype) = tokenizer(html).next().unwrap() else {
        panic!("expected doctype");
    };
    let labels = vec![
        (doctype.public_id_span().unwrap(), "public id"),
        (doctype.system_id_span().unwrap(), "system id"),
    ];
    assert_snapshot!(annotate(html, labels), @r###"
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
                           ^^^^^^^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ system id
                           │
                           public id
    "###);
}

fn annotate_errors(html: &'static str) -> String {
    let mut labels = Vec::new();
    for token in tokenizer(html) {
        if let Token::Error { error, span } = token {
            labels.push((span, error.code()));
        }
    }
    annotate(html, labels)
}

#[test]
fn tests_for_errors_are_sorted() {
    let source_of_this_file = std::fs::read_to_string(file!()).unwrap();
    let mut error_tests: Vec<_> = source_of_this_file
        .lines()
        .filter(|l| l.starts_with("fn error_"))
        .collect();
    let error_tests_found_order = error_tests.join("\n");
    error_tests.sort();
    let error_tests_sorted = error_tests.join("\n");
    assert_eq!(error_tests_found_order, error_tests_sorted);
}

#[test]
fn error_duplicate_attribute() {
    let html = "Does this open two pages? <a href=foo.html href=bar.html>";
    assert_snapshot!(annotate_errors(html), @r###"
    Does this open two pages? <a href=foo.html href=bar.html>
                                               ^^^^ duplicate-attribute
    "###);
}

#[test]
fn error_end_tag_with_attributes() {
    let html = "</end-tag first second=value>";
    assert_snapshot!(annotate_errors(html), @r###"
    </end-tag first second=value>
                    ^^^^^^ end-tag-with-attributes
    "###);
}

#[test]
fn error_end_tag_with_trailing_solidus() {
    let html = "Do you start or do you end? </yes/>";
    assert_snapshot!(annotate_errors(html), @r###"
    Do you start or do you end? </yes/>
                                      ^ end-tag-with-trailing-solidus
    "###);
}

#[test]
fn error_invalid_first_character_of_tag_name() {
    let html = "Please mind the gap: < test";
    assert_snapshot!(annotate_errors(html), @r###"
    Please mind the gap: < test
                          ^ invalid-first-character-of-tag-name
    "###);
}

#[test]
fn error_unknown_named_character_reference() {
    let html = "The pirate says &arrrrr;";
    assert_snapshot!(annotate_errors(html), @r###"
    The pirate says &arrrrr;
                           ^ unknown-named-character-reference
    "###);
}