pub mod display;
mod document;
mod element;
pub mod key;
mod select;
mod value;

pub use document::{Declaration, Document, ReadError};
pub use element::{Element, NewElement};
pub use key::Node;
pub use select::Selector;

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use qname::qname;

    use crate::select::Selector;

    use super::*;

    #[test]
    fn test() {
        use indexmap::IndexMap;

        use crate::document::Document;

        let mut doc = Document::new("root");

        let new_el = doc.root().append_new_element(
            &mut doc,
            NewElement {
                name: qname!("child"),
                attrs: Default::default(),
            },
        );
        new_el.append_new_element(
            &mut doc,
            NewElement {
                name: qname!("child2"),
                attrs: Default::default(),
            },
        );
        let mut attrs = IndexMap::new();
        attrs.insert(qname!("hello"), "yes".into());
        attrs.insert(qname!("another-thing"), "yes".into());

        let foo = new_el.append_new_element(
            &mut doc,
            NewElement {
                name: "with-child2".parse().unwrap(),
                attrs,
            },
        );
        foo.append_new_element(
            &mut doc,
            NewElement {
                name: "child3".parse().unwrap(),
                attrs: Default::default(),
            },
        );
        foo.append_new_element(
            &mut doc,
            NewElement {
                name: "child3".parse().unwrap(),
                attrs: Default::default(),
            },
        );
        foo.append_new_element(
            &mut doc,
            NewElement {
                name: "child3".parse().unwrap(),
                attrs: Default::default(),
            },
        );
        new_el.append_new_element(
            &mut doc,
            NewElement {
                name: "child2".parse().unwrap(),
                attrs: Default::default(),
            },
        );

        let _potato = doc.root().append_new_element(
            &mut doc,
            NewElement {
                name: "potato".parse().unwrap(),
                attrs: Default::default(),
            },
        );

        foo.parent(&doc)
            .unwrap()
            .remove_child(&mut doc, Node::Element(foo));

        assert_eq!(
            "<root><child><child2/><child2/></child><potato/></root>",
            format!("{}", doc)
        );
    }

    #[test]
    fn smoke2() {
        let doc = Document::from_str(
            r#"<root xmlns:x="http://lol" someattr="true">lol <x:sparta/><sparta derp="9000"></sparta> </root>"#,
        ).unwrap();
        assert_eq!("<root xmlns:x=\"http://lol\" someattr=\"true\">lol <x:sparta/><sparta derp=\"9000\"/></root>", format!("{}", doc));
    }

    #[test]
    fn smoke3() {
        let input = r#"<俄语 լեզու="ռուսերեն">данные</俄语>"#;
        let doc = Document::from_str(input).unwrap();

        assert_eq!("<俄语 լեզու=\"ռուսերեն\">данные</俄语>", format!("{}", doc));
    }

    #[test]
    fn smoke4() {
        let input = "<root>ذ&amp;اكرة USB كبيرة السعة التخزينية (عصا، قرص ذاكرة USB)...</root>";
        let doc = Document::from_str(input).unwrap();

        assert_eq!(
            "<root>ذ&amp;اكرة USB كبيرة السعة التخزينية (عصا، قرص ذاكرة USB)...</root>",
            format!("{}", doc)
        );
    }

    #[test]
    fn smoke5() {
        let input = "<root>
            Text text &#x202d;text text
            &#x202e;text text &#x202d;text text
        </root>";
        let doc = Document::from_str(input).unwrap();

        assert_eq!("<root>\n            Text text &#x202D;text text\n            &#x202E;text text &#x202D;text text\n        </root>", format!("{}", doc));
    }

    #[test]
    fn clone() {
        let input = r#"<俄语 լեզու="ռուսերեն">данные</俄语>"#;
        let doc = Document::from_str(input).unwrap();
        let mut doc2 = doc.clone();

        let root = doc2.root();
        root.append_new_element(
            &mut doc2,
            NewElement {
                name: "lol".parse().unwrap(),
                attrs: Default::default(),
            },
        );

        assert_eq!("<俄语 լեզու=\"ռուսերեն\">данные</俄语>", format!("{}", doc));
        assert_eq!(
            "<俄语 լեզու=\"ռուսերեն\">данные<lol/></俄语>",
            format!("{}", doc2)
        );
    }

    #[test]
    fn long_attrs() {
        let input = r#"<root attribute1="potato potato potato"
            attribute2="potato potato potato"
            attribute3="potato potato potato"
            attribute4="potato potato potato"
        >
            <interesting attribute1="potato potato potato" attribute2="potato potato potato"
            />
            <another-one/>
        </root>
        "#;
        let doc = Document::from_str(input).unwrap();
        assert_eq!("<root attribute1=\"potato potato potato\"\n    attribute2=\"potato potato potato\"\n    attribute3=\"potato potato potato\"\n    attribute4=\"potato potato potato\">\n    <interesting attribute1=\"potato potato potato\" attribute2=\"potato potato potato\" />\n    <another-one/>\n</root>\n", format!("{:#4.120}", doc));
        assert_eq!("<root attribute1=\"potato potato potato\"\n  attribute2=\"potato potato potato\"\n  attribute3=\"potato potato potato\"\n  attribute4=\"potato potato potato\">\n  <interesting attribute1=\"potato potato potato\"\n    attribute2=\"potato potato potato\" />\n  <another-one/>\n</root>\n", format!("{:#2.60}", doc));
        assert_eq!("<root attribute1=\"potato potato potato\" attribute2=\"potato potato potato\" attribute3=\"potato potato potato\" attribute4=\"potato potato potato\">\n <interesting attribute1=\"potato potato potato\" attribute2=\"potato potato potato\" />\n <another-one/>\n</root>\n", format!("{:#1.400}", doc));
    }

    #[test]
    fn hmm() {
        let input = "<?xml version=\"1.1\" ?>some random text<![CDATA[<hahaha>]]><!DOCTYPE root ahh ahhhh><!-- pre --><root/><!-- comment --> some other text";
        let doc = Document::from_str(input).unwrap();
        assert_eq!("<?xml version=\"1.1\"?>\nsome random text\n<![CDATA[<hahaha>]]>\n<!DOCTYPE root ahh ahhhh>\n<!-- pre -->\n<root/>\n<!-- comment -->\nsome other text\n", format!("{:#}", doc));
    }

    #[test]
    fn after() {
        let input = "<root><a/><b/><c/><d/></root>";
        let mut doc = Document::from_str(input).unwrap();

        let b = doc
            .root()
            .query_selector(&doc, &Selector::new("b").unwrap())
            .unwrap();
        b.append_new_element_after(&mut doc, ("potato", [("hihi", "oij")]));

        let d = doc
            .root()
            .query_selector(&doc, &Selector::new("d").unwrap())
            .unwrap();
        d.append_new_element_after(&mut doc, ("potato", [("hihi", "oij")]));
        assert_eq!("<root>\n  <a/>\n  <b/>\n  <potato hihi=\"oij\" />\n  <c/>\n  <d/>\n  <potato hihi=\"oij\" />\n</root>\n", format!("{:#}", doc));
    }

    #[test]
    fn after2() {
        let input = r#"<merge xmlns:latin="http://schemas.android.com/apk/res-auto">
            <include latin:keyboardLayout="@xml/key_styles_common" />
            <include latin:keyboardLayout="@xml/row_qwerty4" />
        </merge>"#;
        let mut doc = Document::from_str(input).unwrap();

        let include_selector = Selector::new("include").expect("this selector is fine");
        let rows_include = doc
            .root()
            .query_selector(&doc, &include_selector)
            .expect("there should be an include");

        let row_append = rows_include.append_new_element_after(
            &mut doc,
            NewElement {
                name: qname!("Row"),
                attrs: [].into(),
            },
        );

        row_append.append_new_element(
            &mut doc,
            NewElement {
                name: qname!("include"),
                attrs: [
                    (qname!("latin:keyboardLayout"), "@xml/potato".to_string()),
                    (qname!("latin:keyWidth"), "8.18%p".to_owned()),
                ]
                .into(),
            },
        );
        let row_append = row_append.append_new_element_after(
            &mut doc,
            NewElement {
                name: qname!("Row"),
                attrs: [].into(),
            },
        );

        row_append.append_new_element(
            &mut doc,
            NewElement {
                name: qname!("include"),
                attrs: [
                    (qname!("latin:keyboardLayout"), "@xml/potato".to_string()),
                    (qname!("latin:keyWidth"), "8.18%p".to_owned()),
                ]
                .into(),
            },
        );
        assert_eq!("<merge xmlns:latin=\"http://schemas.android.com/apk/res-auto\">\n  <include latin:keyboardLayout=\"@xml/key_styles_common\" />\n  <Row>\n    <include latin:keyboardLayout=\"@xml/potato\" latin:keyWidth=\"8.18%p\" />\n  </Row>\n  <Row>\n    <include latin:keyboardLayout=\"@xml/potato\" latin:keyWidth=\"8.18%p\" />\n  </Row>\n  <include latin:keyboardLayout=\"@xml/row_qwerty4\" />\n</merge>\n", format!("{:#}", doc));
    }

    #[test]
    fn set_text() {
        let input = "<root><a/></root>";
        let mut doc = Document::from_str(input).unwrap();

        doc.root().set_text(&mut doc, "potato");

        assert_eq!("<root>potato</root>\n", format!("{:#}", doc));
    }

    #[test]
    fn double_use() {
        let input = "<root><a/></root>";
        let mut doc = Document::from_str(input).unwrap();

        let a = doc
            .root()
            .query_selector(&doc, &Selector::new("a").unwrap())
            .unwrap();
        doc.root().remove_child(&mut doc, Node::Element(a));
        assert_eq!(a.parent(&doc), None);

        let inner = doc.root().append_new_element(
            &mut doc,
            NewElement {
                name: qname!("test"),
                attrs: Default::default(),
            },
        );

        inner.append_element(&mut doc, a);
        assert_eq!(
            "<root>\n  <test>\n    <a/>\n  </test>\n</root>\n",
            format!("{:#}", doc)
        );
    }

    #[test]
    fn selector() {
        let input =
            r#"<strings><string name="english_ime_name">Giella Keyboard</string></strings>"#;
        let doc = Document::from_str(input).unwrap();

        let sel = Selector::new(r#"string[name="english_ime_name"]"#).unwrap();
        let _el = doc.root().query_selector(&doc, &sel).unwrap();
    }

    #[test]
    fn non_root_empty_element_name() {
        let input = r#"<root><elem/><x:elem/></root>"#;
        let doc = Document::from_str(input).unwrap();

        let nq_elem = doc.root().children(&doc)[0];
        assert_eq!(nq_elem.qname(&doc).namespace(), None);
        assert_eq!(nq_elem.prefix(&doc), None);
        assert_eq!(nq_elem.qname(&doc).local_part(), "elem");
        assert_eq!(nq_elem.name(&doc), "elem");

        let q_elem = doc.root().children(&doc)[1];
        assert_eq!(q_elem.qname(&doc).namespace(), Some("x"));
        assert_eq!(q_elem.prefix(&doc), Some("x"));
        assert_eq!(q_elem.qname(&doc).local_part(), "elem");
        assert_eq!(q_elem.name(&doc), "x:elem");
    }

    #[test]
    fn non_root_non_empty_element_name() {
        let input = r#"<root><elem></elem><x:elem></x:elem></root>"#;
        let doc = Document::from_str(input).unwrap();

        let nq_elem = doc.root().children(&doc)[0];
        assert_eq!(nq_elem.qname(&doc).namespace(), None);
        assert_eq!(nq_elem.prefix(&doc), None);
        assert_eq!(nq_elem.qname(&doc).local_part(), "elem");
        assert_eq!(nq_elem.name(&doc), "elem");

        let q_elem = doc.root().children(&doc)[1];
        assert_eq!(q_elem.qname(&doc).namespace(), Some("x"));
        assert_eq!(q_elem.prefix(&doc), Some("x"));
        assert_eq!(q_elem.qname(&doc).local_part(), "elem");
        assert_eq!(q_elem.name(&doc), "x:elem");
    }

    #[test]
    fn set_attribute() {
        let mut doc = Document::from_str("<foo>").unwrap();
        doc.root().set_attribute(&mut doc, "bar", "baz");
        assert_eq!(doc.root().attribute(&doc, "bar"), Some("baz"));
    }

    #[test]
    fn pretty_minimizes_whitespace() {
        let doc = Document::from_str("<text>\n    Actual Output\n    </text>").unwrap();
        assert_eq!(doc.to_string_pretty(), "<text>Actual Output</text>\n");
    }

    #[test]
    fn collapse_simple_text_element() {
        let doc = Document::from_str("<text>\n    Actual Output\n    </text>").unwrap();
        assert_eq!(doc.to_string_pretty(), "<text>Actual Output</text>\n");
    }

    #[test]
    fn collapse_nested_anchor_in_li() {
        // The <li> still nests its child element, but the <a>'s text content stays inline.
        let doc = Document::from_str(
            r#"<li><a href="https://en.wikipedia.org/wiki/Earth">Earth</a></li>"#,
        )
        .unwrap();
        assert_eq!(
            doc.to_string_pretty(),
            "<li>\n  <a href=\"https://en.wikipedia.org/wiki/Earth\">Earth</a>\n</li>\n"
        );
    }

    #[test]
    fn mixed_content_still_nests() {
        // An element containing a child element keeps nesting; only the inner text-only
        // element (<b>) collapses inline.
        let doc = Document::from_str("<p>Some <b>bold</b> text</p>").unwrap();
        assert_eq!(
            doc.to_string_pretty(),
            "<p>\n  Some\n  <b>bold</b>\n  text\n</p>\n"
        );
    }

    #[test]
    fn collapse_preserves_cdata_literally() {
        let doc = Document::from_str("<a><![CDATA[ raw <b> ]]></a>").unwrap();
        assert_eq!(doc.to_string_pretty(), "<a><![CDATA[ raw <b> ]]></a>\n");
    }

    #[test]
    fn collapse_internal_newline_forces_multiline() {
        let doc = Document::from_str("<text>Actual\nOutput</text>").unwrap();
        // A trimmed text node that still contains a newline must not collapse.
        assert_ne!(doc.to_string_pretty(), "<text>Actual\nOutput</text>\n");
    }

    #[test]
    fn text_breaks_when_too_long() {
        use crate::display::Config;
        // Text content longer than max_line_length breaks onto its own line.
        let doc = Document::from_str("<text>this text is fairly long</text>").unwrap();
        let cfg = Config::default_pretty().max_line_length(10);
        assert_eq!(
            doc.to_string_pretty_with_config(&cfg),
            "<text>\n  this text is fairly long\n</text>\n"
        );
    }

    #[test]
    fn child_elements_nest_text_inlines() {
        // The container nests its child elements; each text-only child stays inline.
        let doc =
            Document::from_str("<outer><inner>short</inner><inner>also short</inner></outer>")
                .unwrap();
        let out = doc.to_string_pretty();
        assert_eq!(
            out,
            "<outer>\n  <inner>short</inner>\n  <inner>also short</inner>\n</outer>\n"
        );
    }

    #[test]
    fn collapse_tiny_limit_reproduces_old_layout() {
        use crate::display::Config;
        let doc = Document::from_str("<text>\n    Actual Output\n    </text>").unwrap();
        let cfg = Config::default_pretty().max_line_length(0);
        assert_eq!(
            doc.to_string_pretty_with_config(&cfg),
            "<text>\n  Actual Output\n</text>\n"
        );
    }

    #[test]
    fn collapse_roundtrip_stable() {
        let input = r#"<li><a href="https://en.wikipedia.org/wiki/Earth">Earth</a></li>"#;
        let once = Document::from_str(input).unwrap().to_string_pretty();
        let twice = Document::from_str(&once).unwrap().to_string_pretty();
        assert_eq!(once, twice);
    }

    #[test]
    fn tight_collapses_nested_subtree() {
        use crate::display::Config;
        let doc = Document::from_str(
            r#"<li><a href="https://en.wikipedia.org/wiki/Earth">Earth</a></li>"#,
        )
        .unwrap();
        let cfg = Config::default_pretty().tight();
        assert_eq!(
            doc.to_string_pretty_with_config(&cfg),
            "<li><a href=\"https://en.wikipedia.org/wiki/Earth\">Earth</a></li>\n"
        );
    }

    #[test]
    fn tight_breaks_when_too_long() {
        use crate::display::Config;
        // The subtree exceeds the limit, so it falls back to nesting; the text-only children
        // still collapse inline.
        let doc = Document::from_str("<root><a>one</a><b>two</b></root>").unwrap();
        let cfg = Config::default_pretty().tight().max_line_length(20);
        assert_eq!(
            doc.to_string_pretty_with_config(&cfg),
            "<root>\n  <a>one</a>\n  <b>two</b>\n</root>\n"
        );
    }

    #[test]
    fn tight_roundtrip_stable() {
        use crate::display::Config;
        let cfg = Config::default_pretty().tight();
        let input = r#"<li><a href="https://en.wikipedia.org/wiki/Earth">Earth</a></li>"#;
        let once = Document::from_str(input)
            .unwrap()
            .to_string_pretty_with_config(&cfg);
        let twice = Document::from_str(&once)
            .unwrap()
            .to_string_pretty_with_config(&cfg);
        assert_eq!(once, twice);
    }

    #[test]
    fn tight_default_off_still_nests() {
        // Without tight(), the default text-only inlining keeps child elements nested.
        let doc = Document::from_str(
            r#"<li><a href="https://en.wikipedia.org/wiki/Earth">Earth</a></li>"#,
        )
        .unwrap();
        assert_eq!(
            doc.to_string_pretty(),
            "<li>\n  <a href=\"https://en.wikipedia.org/wiki/Earth\">Earth</a>\n</li>\n"
        );
    }

    #[test]
    fn non_pretty_preserves_whitespace() {
        const EXACT_XML: &str = "<text>\t  \n Actual \n \t Output   \t\n  </text>";
        let doc = Document::from_str(EXACT_XML).unwrap();
        assert_eq!(doc.to_string(), EXACT_XML);
    }

    #[test]
    fn decoded_entity() {
        let input = "<root>an &amp;&#x20;here</root>";
        let doc = Document::from_str(input).unwrap();
        let root = doc.root();
        let root_children = root.child_nodes(&doc);
        assert_eq!(root_children.len(), 1);
        let Node::Text(text) = root_children[0] else {
            panic!("Expected child to be a text node");
        };
        assert_eq!(text.as_str(&doc), "an & here");
    }

    #[test]
    fn char_0_roundtrip() {
        const EXACT_XML: &str = "<text>&#x0000;</text>";
        let doc = Document::from_str(EXACT_XML).unwrap();
        assert_eq!(doc.to_string(), EXACT_XML);
    }

    #[test]
    fn accepts_pi_before_root() {
        Document::from_str(r#"<?xml-stylesheet href="style.css" type="text/css"?><root/>"#)
            .unwrap();
    }

    fn parse_buffer(buf: &[u8]) -> Result<Document, ReadError> {
        Document::from_reader(std::io::Cursor::new(buf))
    }

    #[test]
    fn ignored_invalids() {
        parse_buffer(b"<?xml version=\"\xA1\"?><root/>").unwrap();
        parse_buffer(b"<?xml other?><root/>").unwrap();
        parse_buffer(b"<?xml version=\"1.1\" standalone=\"\xA1\"?><root/>").unwrap();
        parse_buffer(b"<?xml version=\"1.1\" encoding=\"\xA1\"?><root/>").unwrap();
        parse_buffer(b"<!DOCTYPE a=\"&\"><root/>").unwrap();
    }

    #[test]
    fn erroring_invalids() {
        parse_buffer(b"").unwrap_err();
        parse_buffer(b"</root>").unwrap_err();
        parse_buffer(b"<!DOCTYPE \xA1><root/>").unwrap_err();
        parse_buffer(b"&").unwrap_err();
        parse_buffer(b"<!-- & -->").unwrap_err();
        parse_buffer(b"<\xA1/>").unwrap_err();
        parse_buffer(b"<\x00/>").unwrap_err();
        parse_buffer(b"<root a=\"&\"/>").unwrap_err();
        parse_buffer(b"<root a=\"\xA1\"/>").unwrap_err();
        parse_buffer(b"<root \xA1=\"\"/>").unwrap_err();
        parse_buffer(b"<root><\xA1/></root>").unwrap_err();
        parse_buffer(b"<root><\x00/></root>").unwrap_err();
        parse_buffer(b"<root><elem a=\"\xA1\"/></root>").unwrap_err();
        parse_buffer(b"<root><elem a=\"&\"/></root>").unwrap_err();
        parse_buffer(b"<root><elem \xA1=\"\"/></root>").unwrap_err();
        parse_buffer(b"<root><elem \x00=\"\"/></root>").unwrap_err();
        parse_buffer(b"<root><\xA1></\xA1></root>").unwrap_err();
        parse_buffer(b"<root><\x00></\x00></root>").unwrap_err();
        parse_buffer(b"<root><elem a=\"\xA1\"></elem></root>").unwrap_err();
        parse_buffer(b"<root><elem a=\"&\"></elem></root>").unwrap_err();
        parse_buffer(b"<root><elem \xA1=\"\"></elem></root>").unwrap_err();
        parse_buffer(b"<root><elem \x00=\"\"></elem></root>").unwrap_err();
        parse_buffer(b"<a <=\"\"/>").unwrap_err();
    }

    #[test]
    fn test_ampersand_in_comment() {
        let input = r#"<root><!-- This & that --></root>"#;
        let doc = Document::from_str(input).unwrap();
        let out = format!("{}", doc);
        assert_eq!(out, "<root><!-- This & that --></root>");
    }
}
