/*! This is Typst's test runner.

Tests are Typst files composed of a header part followed by subtests.

The header may contain:
- a small description `// tests that features X works well`
- metadata (see [metadata::TestConfiguration])

The subtests may use extra testing functions defined in [library], most
importantly, `test(x, y)` which will fail the test `if x != y`.
*/

#![allow(clippy::comparison_chain)]
mod metadata;

use self::metadata::*;

use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::ffi::OsStr;
use std::fmt::Write as _;
use std::io::{self, IsTerminal, Write as _};
use std::ops::Range;
use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
use std::sync::{OnceLock, RwLock};
use std::{env, fs};

use clap::Parser;
use comemo::{Prehashed, Track};
use oxipng::{InFile, Options, OutFile};
use rayon::iter::{ParallelBridge, ParallelIterator};
use tiny_skia as sk;
use typst::diag::{bail, FileError, FileResult, Severity, SourceDiagnostic, StrResult};
use typst::eval::Tracer;
use typst::foundations::{func, Bytes, Datetime, NoneValue, Repr, Smart, Value};
use typst::introspection::Meta;
use typst::layout::{Abs, Frame, FrameItem, Margin, Page, PageElem, Transform};
use typst::model::Document;
use typst::syntax::{FileId, Source, SyntaxNode, VirtualPath};
use typst::text::{Font, FontBook, TextElem, TextSize};
use typst::visualize::Color;
use typst::{Library, World, WorldExt};
use walkdir::WalkDir;

// These directories are all relative to the tests/ directory.
const TYP_DIR: &str = "typ";
const REF_DIR: &str = "ref";
const PNG_DIR: &str = "png";
const PDF_DIR: &str = "pdf";
const SVG_DIR: &str = "svg";

/// Arguments that modify test behaviour.
///
/// Specify them like this when developing:
/// `cargo test --workspace --test tests -- --help`
#[derive(Debug, Clone, Parser)]
#[clap(name = "typst-test", author)]
struct Args {
    /// All the tests that contains a filter string will be run (unless
    /// `--exact` is specified, which is even stricter).
    filter: Vec<String>,
    /// Runs only the specified subtest.
    #[arg(short, long)]
    #[arg(allow_hyphen_values = true)]
    subtest: Option<isize>,
    /// Runs only the test with the exact name specified in your command.
    ///
    /// Example:
    /// `cargo test --workspace --test tests  -- compiler/bytes.typ --exact`
    #[arg(long)]
    exact: bool,
    /// Updates the reference images in `tests/ref`.
    #[arg(long, default_value_t = env::var_os("UPDATE_EXPECT").is_some())]
    update: bool,
    /// Exports the tests as PDF into `tests/pdf`.
    #[arg(long)]
    pdf: bool,
    /// Configuration of what to print.
    #[command(flatten)]
    print: PrintConfig,
    /// Running `cargo test --workspace -- --nocapture` for the unit tests would
    /// fail the test runner without argument.
    // TODO: would it really still happen?
    #[arg(long)]
    nocapture: bool,
    /// Prevents the terminal from being cleared of test names and includes
    /// non-essential test messages.
    #[arg(short, long)]
    verbose: bool,
}

/// Which things to print out for debugging.
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Parser)]
struct PrintConfig {
    /// Print the syntax tree.
    #[arg(long)]
    syntax: bool,
    /// Print the content model.
    #[arg(long)]
    model: bool,
    /// Print the layouted frames.
    #[arg(long)]
    frames: bool,
}

impl Args {
    fn matches(&self, canonicalized_path: &Path) -> bool {
        let path = canonicalized_path.to_string_lossy();
        if !self.exact {
            return self.filter.is_empty()
                || self.filter.iter().any(|v| path.contains(v));
        }

        self.filter.iter().any(|v| match path.strip_suffix(v) {
            None => false,
            Some(residual) => {
                residual.is_empty() || residual.ends_with(MAIN_SEPARATOR_STR)
            }
        })
    }
}

/// Tests all test files and prints a summary.
fn main() {
    let args = Args::parse();

    // Create loader and context.
    let world = TestWorld::new(args.print);

    println!("Running tests...");
    let results = WalkDir::new(TYP_DIR)
        .sort_by_file_name()
        .into_iter()
        .par_bridge()
        .filter_map(|entry| {
            let entry = entry.unwrap();
            if entry.depth() == 0 {
                return None;
            }

            if entry.path().starts_with("typ/benches") {
                return None;
            }

            let src_path = entry.into_path(); // Relative to TYP_DIR.
            if src_path.extension() != Some(OsStr::new("typ")) {
                return None;
            }

            if args.matches(&src_path.canonicalize().unwrap()) {
                Some(src_path)
            } else {
                None
            }
        })
        .map_with(world, |world, src_path| {
            let path = src_path.strip_prefix(TYP_DIR).unwrap();
            let png_path = Path::new(PNG_DIR).join(path).with_extension("png");
            let ref_path = Path::new(REF_DIR).join(path).with_extension("png");
            let svg_path = Path::new(SVG_DIR).join(path).with_extension("svg");
            let pdf_path =
                args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf"));

            test(
                world,
                &src_path,
                &png_path,
                &ref_path,
                pdf_path.as_deref(),
                &svg_path,
                &args,
            ) as usize
        })
        .collect::<Vec<_>>();

    let len = results.len();
    let ok = results.iter().sum::<usize>();
    if len > 0 {
        println!("{ok} / {len} test{} passed.", if len > 1 { "s" } else { "" });
    } else {
        println!("No test ran.");
    }

    if ok != len {
        println!(
            "Set the UPDATE_EXPECT environment variable or pass the \
             --update flag to update the reference image(s)."
        );
    }

    if ok < len {
        std::process::exit(1);
    }
}

fn library() -> Library {
    #[func]
    fn test(lhs: Value, rhs: Value) -> StrResult<NoneValue> {
        if lhs != rhs {
            bail!("Assertion failed: {} != {}", lhs.repr(), rhs.repr());
        }
        Ok(NoneValue)
    }

    #[func]
    fn test_repr(lhs: Value, rhs: Value) -> StrResult<NoneValue> {
        if lhs.repr() != rhs.repr() {
            bail!("Assertion failed: {} != {}", lhs.repr(), rhs.repr());
        }
        Ok(NoneValue)
    }

    #[func]
    fn print(#[variadic] values: Vec<Value>) -> NoneValue {
        let mut stdout = io::stdout().lock();
        write!(stdout, "> ").unwrap();
        for (i, value) in values.into_iter().enumerate() {
            if i > 0 {
                write!(stdout, ", ").unwrap();
            }
            write!(stdout, "{value:?}").unwrap();
        }
        writeln!(stdout).unwrap();
        NoneValue
    }

    // Set page width to 120pt with 10pt margins, so that the inner page is
    // exactly 100pt wide. Page height is unbounded and font size is 10pt so
    // that it multiplies to nice round numbers.
    let mut lib = Library::default();
    lib.styles
        .set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
    lib.styles.set(PageElem::set_height(Smart::Auto));
    lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom(
        Abs::pt(10.0).into(),
    )))));
    lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into())));

    // Hook up helpers into the global scope.
    lib.global.scope_mut().define_func::<test>();
    lib.global.scope_mut().define_func::<test_repr>();
    lib.global.scope_mut().define_func::<print>();
    lib.global
        .scope_mut()
        .define("conifer", Color::from_u8(0x9f, 0xEB, 0x52, 0xFF));
    lib.global
        .scope_mut()
        .define("forest", Color::from_u8(0x43, 0xA1, 0x27, 0xFF));

    lib
}

/// A world that provides access to the tests environment.
struct TestWorld {
    print: PrintConfig,
    main: FileId,
    library: Prehashed<Library>,
    book: Prehashed<FontBook>,
    fonts: Vec<Font>,
    slots: RwLock<HashMap<FileId, FileSlot>>,
}

#[derive(Clone)]
struct FileSlot {
    source: OnceLock<FileResult<Source>>,
    buffer: OnceLock<FileResult<Bytes>>,
}

impl TestWorld {
    fn new(print: PrintConfig) -> Self {
        let fonts: Vec<_> = typst_assets::fonts()
            .chain(typst_dev_assets::fonts())
            .flat_map(|data| Font::iter(Bytes::from_static(data)))
            .collect();

        Self {
            print,
            main: FileId::new(None, VirtualPath::new("main.typ")),
            library: Prehashed::new(library()),
            book: Prehashed::new(FontBook::from_fonts(&fonts)),
            fonts,
            slots: RwLock::new(HashMap::new()),
        }
    }
}

impl World for TestWorld {
    fn library(&self) -> &Prehashed<Library> {
        &self.library
    }

    fn book(&self) -> &Prehashed<FontBook> {
        &self.book
    }

    fn main(&self) -> Source {
        self.source(self.main).unwrap()
    }

    fn source(&self, id: FileId) -> FileResult<Source> {
        self.slot(id, |slot| {
            slot.source
                .get_or_init(|| {
                    let buf = read(&system_path(id)?)?;
                    let text = String::from_utf8(buf.into_owned())?;
                    Ok(Source::new(id, text))
                })
                .clone()
        })
    }

    fn file(&self, id: FileId) -> FileResult<Bytes> {
        self.slot(id, |slot| {
            slot.buffer
                .get_or_init(|| {
                    read(&system_path(id)?).map(|cow| match cow {
                        Cow::Owned(buf) => buf.into(),
                        Cow::Borrowed(buf) => Bytes::from_static(buf),
                    })
                })
                .clone()
        })
    }

    fn font(&self, id: usize) -> Option<Font> {
        Some(self.fonts[id].clone())
    }

    fn today(&self, _: Option<i64>) -> Option<Datetime> {
        Some(Datetime::from_ymd(1970, 1, 1).unwrap())
    }
}

impl TestWorld {
    fn set(&mut self, path: &Path, text: String) -> Source {
        self.main = FileId::new(None, VirtualPath::new(path));
        let source = Source::new(self.main, text);
        self.slot(self.main, |slot| {
            slot.source = OnceLock::from(Ok(source.clone()));
            source
        })
    }

    fn slot<F, T>(&self, id: FileId, f: F) -> T
    where
        F: FnOnce(&mut FileSlot) -> T,
    {
        f(self.slots.write().unwrap().entry(id).or_insert_with(|| FileSlot {
            source: OnceLock::new(),
            buffer: OnceLock::new(),
        }))
    }
}

impl Clone for TestWorld {
    fn clone(&self) -> Self {
        Self {
            print: self.print,
            main: self.main,
            library: self.library.clone(),
            book: self.book.clone(),
            fonts: self.fonts.clone(),
            slots: RwLock::new(self.slots.read().unwrap().clone()),
        }
    }
}

/// The file system path for a file ID.
fn system_path(id: FileId) -> FileResult<PathBuf> {
    let root: PathBuf = match id.package() {
        Some(spec) => format!("packages/{}-{}", spec.name, spec.version).into(),
        None => PathBuf::new(),
    };

    id.vpath().resolve(&root).ok_or(FileError::AccessDenied)
}

/// Read a file.
fn read(path: &Path) -> FileResult<Cow<'static, [u8]>> {
    // Basically symlinks `assets/files` to `tests/files` so that the assets
    // are within the test project root.
    let resolved = path.to_path_buf();
    if let Ok(suffix) = path.strip_prefix("assets/") {
        return typst_dev_assets::get(&suffix.to_string_lossy())
            .map(Cow::Borrowed)
            .ok_or_else(|| FileError::NotFound(path.into()));
    }

    let f = |e| FileError::from_io(e, path);
    if fs::metadata(&resolved).map_err(f)?.is_dir() {
        Err(FileError::IsDirectory)
    } else {
        fs::read(&resolved).map(Cow::Owned).map_err(f)
    }
}

/// Tests a test file and prints the result.
///
/// Also tests that the header of each test is written correctly.
/// See [parse_part_metadata] for more details.
fn test(
    world: &mut TestWorld,
    src_path: &Path,
    png_path: &Path,
    ref_path: &Path,
    pdf_path: Option<&Path>,
    svg_path: &Path,
    args: &Args,
) -> bool {
    struct PanicGuard<'a>(&'a Path);
    impl Drop for PanicGuard<'_> {
        fn drop(&mut self) {
            if std::thread::panicking() {
                println!("Panicked in {}", self.0.display());
            }
        }
    }

    let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path);
    let text = fs::read_to_string(src_path).unwrap();
    let _guard = PanicGuard(name);

    let mut output = String::new();
    let mut ok = true;
    let mut updated = false;
    let mut pages = vec![];
    let mut line = 0;
    let mut header_configuration = None;
    let mut compare_ever = false;
    let mut rng = LinearShift::new();

    let parts: Vec<_> = text
        .split("\n---")
        .map(|s| s.strip_suffix('\r').unwrap_or(s))
        .collect();

    for (i, &part) in parts.iter().enumerate() {
        if let Some(x) = args.subtest {
            let x = usize::try_from(
                x.rem_euclid(isize::try_from(parts.len()).unwrap_or_default()),
            )
            .unwrap();
            if x != i {
                writeln!(output, "  Skipped subtest {i}.").unwrap();
                continue;
            }
        }
        let is_header = i == 0
            && parts.len() > 1
            && part
                .lines()
                .all(|s| s.starts_with("//") || s.chars().all(|c| c.is_whitespace()));

        if is_header {
            let source = Source::detached(part.to_string());
            let metadata = parse_part_metadata(&source, true);
            match metadata {
                Ok(metadata) => {
                    header_configuration = Some(metadata.config);
                }
                Err(invalid_data) => {
                    ok = false;
                    writeln!(
                        output,
                        " Test {}: invalid metadata in header, failing the test:",
                        name.display()
                    )
                    .unwrap();
                    InvalidMetadata::write(
                        invalid_data,
                        &mut output,
                        &mut |annotation, output| {
                            print_annotation(output, &source, line, annotation)
                        },
                    );
                }
            }
        } else {
            let (part_ok, compare_here, part_frames) = test_part(
                &mut output,
                world,
                src_path,
                part.into(),
                line,
                i,
                header_configuration.as_ref().unwrap_or(&Default::default()),
                &mut rng,
                args.verbose,
            );

            ok &= part_ok;
            compare_ever |= compare_here;
            pages.extend(part_frames);
        }

        line += part.lines().count() + 1;
    }

    let document = Document { pages, ..Default::default() };
    if compare_ever {
        if let Some(pdf_path) = pdf_path {
            let pdf_data = typst_pdf::pdf(
                &document,
                Smart::Custom(&format!("typst-test: {}", name.display())),
                world.today(Some(0)),
            );
            fs::create_dir_all(pdf_path.parent().unwrap()).unwrap();
            fs::write(pdf_path, pdf_data).unwrap();
        }

        if world.print.frames {
            for frame in &document.pages {
                writeln!(output, "{frame:#?}\n").unwrap();
            }
        }

        let canvas = render(&document);
        fs::create_dir_all(png_path.parent().unwrap()).unwrap();
        canvas.save_png(png_path).unwrap();

        let svg = typst_svg::svg_merged(&document, Abs::pt(5.0));

        fs::create_dir_all(svg_path.parent().unwrap()).unwrap();
        std::fs::write(svg_path, svg.as_bytes()).unwrap();

        if let Ok(ref_pixmap) = sk::Pixmap::load_png(ref_path) {
            if canvas.width() != ref_pixmap.width()
                || canvas.height() != ref_pixmap.height()
                || canvas
                    .data()
                    .iter()
                    .zip(ref_pixmap.data())
                    .any(|(&a, &b)| a.abs_diff(b) > 2)
            {
                if args.update {
                    update_image(png_path, ref_path);
                    updated = true;
                } else {
                    writeln!(output, "  Does not match reference image.").unwrap();
                    ok = false;
                }
            }
        } else if !document.pages.is_empty() {
            if args.update {
                update_image(png_path, ref_path);
                updated = true;
            } else {
                writeln!(output, "  Failed to open reference image.").unwrap();
                ok = false;
            }
        }
    }

    {
        let mut stdout = io::stdout().lock();
        stdout.write_all(name.to_string_lossy().as_bytes()).unwrap();
        if ok {
            writeln!(stdout, " ✔").unwrap();
            // Don't clear the line when in verbose mode or when the reference image
            // was updated, to show in the output which test had its image updated.
            if !updated && !args.verbose && stdout.is_terminal() {
                // ANSI escape codes: cursor moves up and clears the line.
                write!(stdout, "\x1b[1A\x1b[2K").unwrap();
            }
        } else {
            writeln!(stdout, " ❌").unwrap();
        }
        if updated {
            writeln!(stdout, "  Updated reference image.").unwrap();
        }
        if !output.is_empty() {
            stdout.write_all(output.as_bytes()).unwrap();
        }
    }

    ok
}

fn update_image(png_path: &Path, ref_path: &Path) {
    oxipng::optimize(
        &InFile::Path(png_path.to_owned()),
        &OutFile::from_path(ref_path.to_owned()),
        &Options::max_compression(),
    )
    .unwrap();
}

#[allow(clippy::too_many_arguments)]
fn test_part(
    output: &mut String,
    world: &mut TestWorld,
    src_path: &Path,
    text: String,
    line: usize,
    i: usize,
    header_configuration: &TestConfig,
    rng: &mut LinearShift,
    verbose: bool,
) -> (bool, bool, Vec<Page>) {
    let source = world.set(src_path, text);
    if world.print.syntax {
        writeln!(output, "Syntax Tree:\n{:#?}\n", source.root()).unwrap();
    }

    if world.print.model {
        print_model(world, &source, output);
    }

    let mut tracer = Tracer::new();
    let (mut frames, diagnostics) = match typst::compile(world, &mut tracer) {
        Ok(document) => (document.pages, tracer.warnings()),
        Err(errors) => {
            let mut warnings = tracer.warnings();
            warnings.extend(errors);
            (vec![], warnings)
        }
    };

    let metadata = parse_part_metadata(&source, false);
    match metadata {
        Ok(metadata) => {
            let mut ok = true;
            let compare_ref = metadata
                .config
                .compare_ref
                .unwrap_or(header_configuration.compare_ref.unwrap_or(true));
            let validate_hints = metadata
                .config
                .validate_hints
                .unwrap_or(header_configuration.validate_hints.unwrap_or(true));
            let validate_autocomplete = metadata
                .config
                .validate_autocomplete
                .unwrap_or(header_configuration.validate_autocomplete.unwrap_or(false));

            if verbose {
                writeln!(output, "Subtest {i} runs with compare_ref={compare_ref}; validate_hints={validate_hints}; validate_autocomplete={validate_autocomplete};").unwrap();
            }
            ok &= test_spans(output, source.root());
            ok &= test_reparse(output, source.text(), i, rng);

            // Don't retain frames if we don't want to compare with reference images.
            if !compare_ref {
                frames.clear();
            }

            // we never check autocomplete and error at the same time

            let diagnostic_annotations = metadata
                .annotations
                .iter()
                .filter(|a| {
                    !matches!(
                        a.kind,
                        AnnotationKind::AutocompleteContains
                            | AnnotationKind::AutocompleteExcludes
                    )
                })
                .cloned()
                .collect::<HashSet<_>>();

            if validate_autocomplete {
                // warns and ignores diagnostics
                if !diagnostic_annotations.is_empty() {
                    writeln!(
                        output,
                        "  Subtest {i} contains diagnostics but is in autocomplete mode."
                    )
                    .unwrap();
                    for annotation in diagnostic_annotations {
                        write!(output, "    Ignored | ").unwrap();
                        print_annotation(output, &source, line, &annotation);
                    }
                }

                test_autocomplete(
                    output,
                    world,
                    &source,
                    line,
                    i,
                    &mut ok,
                    metadata.annotations.iter(),
                );
            } else {
                test_diagnostics(
                    output,
                    world,
                    &source,
                    line,
                    i,
                    &mut ok,
                    validate_hints,
                    diagnostics.iter(),
                    &diagnostic_annotations,
                );
            }

            (ok, compare_ref, frames)
        }
        Err(invalid_data) => {
            writeln!(output, "  Subtest {i} has invalid metadata, failing the test:")
                .unwrap();
            InvalidMetadata::write(
                invalid_data,
                output,
                &mut |annotation: &Annotation, output: &mut String| {
                    print_annotation(output, &source, line, annotation)
                },
            );

            (false, false, frames)
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn test_autocomplete<'a>(
    output: &mut String,
    world: &mut TestWorld,
    source: &Source,
    line: usize,
    i: usize,
    ok: &mut bool,
    annotations: impl Iterator<Item = &'a Annotation>,
) {
    for annotation in annotations.filter(|a| {
        matches!(
            a.kind,
            AnnotationKind::AutocompleteContains | AnnotationKind::AutocompleteExcludes
        )
    }) {
        // Ok cause we checked in parsing that range was Some for this annotation
        let cursor = annotation.range.as_ref().unwrap().start;

        // todo, use document if is_some to test labels autocomplete
        let completions = typst_ide::autocomplete(world, None, source, cursor, true)
            .map(|(_, c)| c)
            .unwrap_or_default()
            .into_iter()
            .map(|c| c.label.to_string())
            .collect::<HashSet<_>>();
        let completions =
            completions.iter().map(|s| s.as_str()).collect::<HashSet<&str>>();

        let must_contain_or_exclude = parse_string_list(&annotation.text);
        let missing =
            must_contain_or_exclude.difference(&completions).collect::<Vec<_>>();

        if !missing.is_empty()
            && matches!(annotation.kind, AnnotationKind::AutocompleteContains)
        {
            writeln!(output, "  Subtest {i} does not match expected completions.")
                .unwrap();
            write!(output, "  for annotation  | ").unwrap();
            print_annotation(output, source, line, annotation);

            write!(output, "    Not contained  // ").unwrap();
            for item in missing {
                write!(output, "{item:?}, ").unwrap()
            }
            writeln!(output).unwrap();
            *ok = false;
        }

        let undesired =
            must_contain_or_exclude.intersection(&completions).collect::<Vec<_>>();

        if !undesired.is_empty()
            && matches!(annotation.kind, AnnotationKind::AutocompleteExcludes)
        {
            writeln!(output, "  Subtest {i} does not match expected completions.")
                .unwrap();
            write!(output, "  for annotation  | ").unwrap();
            print_annotation(output, source, line, annotation);

            write!(output, "    Not excluded  // ").unwrap();
            for item in undesired {
                write!(output, "{item:?}, ").unwrap()
            }
            writeln!(output).unwrap();
            *ok = false;
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn test_diagnostics<'a>(
    output: &mut String,
    world: &mut TestWorld,
    source: &Source,
    line: usize,
    i: usize,
    ok: &mut bool,
    validate_hints: bool,
    diagnostics: impl Iterator<Item = &'a SourceDiagnostic>,
    diagnostic_annotations: &HashSet<Annotation>,
) {
    // Map diagnostics to range and message format, discard traces and errors from
    // other files, collect hints.
    //
    // This has one caveat: due to the format of the expected hints, we can not
    // verify if a hint belongs to a diagnostic or not. That should be irrelevant
    // however, as the line of the hint is still verified.
    let mut actual_diagnostics = HashSet::new();
    for diagnostic in diagnostics {
        // Ignore diagnostics from other files.
        if diagnostic.span.id().is_some_and(|id| id != source.id()) {
            continue;
        }

        let annotation = Annotation {
            kind: match diagnostic.severity {
                Severity::Error => AnnotationKind::Error,
                Severity::Warning => AnnotationKind::Warning,
            },
            range: world.range(diagnostic.span),
            text: diagnostic.message.replace("\\", "/"),
        };

        if validate_hints {
            for hint in &diagnostic.hints {
                actual_diagnostics.insert(Annotation {
                    kind: AnnotationKind::Hint,
                    text: hint.clone(),
                    range: annotation.range.clone(),
                });
            }
        }

        actual_diagnostics.insert(annotation);
    }

    // Basically symmetric_difference, but we need to know where an item is coming from.
    let mut unexpected_outputs = actual_diagnostics
        .difference(diagnostic_annotations)
        .collect::<Vec<_>>();
    let mut missing_outputs = diagnostic_annotations
        .difference(&actual_diagnostics)
        .collect::<Vec<_>>();

    unexpected_outputs.sort_by_key(|&v| v.range.as_ref().map(|r| r.start));
    missing_outputs.sort_by_key(|&v| v.range.as_ref().map(|r| r.start));

    // This prints all unexpected emits first, then all missing emits.
    // Is this reasonable or subject to change?
    if !(unexpected_outputs.is_empty() && missing_outputs.is_empty()) {
        writeln!(output, "  Subtest {i} does not match expected errors.").unwrap();
        *ok = false;

        for unexpected in unexpected_outputs {
            write!(output, "    Not annotated // ").unwrap();
            print_annotation(output, source, line, unexpected)
        }

        for missing in missing_outputs {
            write!(output, "    Not emitted   // ").unwrap();
            print_annotation(output, source, line, missing)
        }
    }
}

fn print_model(world: &mut TestWorld, source: &Source, output: &mut String) {
    let world = (world as &dyn World).track();
    let route = typst::engine::Route::default();
    let mut tracer = typst::eval::Tracer::new();

    let module =
        typst::eval::eval(world, route.track(), tracer.track_mut(), source).unwrap();
    writeln!(output, "Model:\n{:#?}\n", module.content()).unwrap();
}

fn print_annotation(
    output: &mut String,
    source: &Source,
    line: usize,
    annotation: &Annotation,
) {
    let Annotation { range, text, kind } = annotation;
    write!(output, "{kind}: ").unwrap();
    if let Some(range) = range {
        let start_line = 1 + line + source.byte_to_line(range.start).unwrap();
        let start_col = 1 + source.byte_to_column(range.start).unwrap();
        let end_line = 1 + line + source.byte_to_line(range.end).unwrap();
        let end_col = 1 + source.byte_to_column(range.end).unwrap();
        write!(output, "{start_line}:{start_col}-{end_line}:{end_col} ").unwrap();
    }
    writeln!(output, "{text}").unwrap();
}

/// Pseudorandomly edit the source file and test whether a reparse produces the
/// same result as a clean parse.
///
/// The method will first inject 10 strings once every 400 source characters
/// and then select 5 leaf node boundaries to inject an additional, randomly
/// chosen string from the injection list.
fn test_reparse(
    output: &mut String,
    text: &str,
    i: usize,
    rng: &mut LinearShift,
) -> bool {
    let supplements = [
        "[",
        "]",
        "{",
        "}",
        "(",
        ")",
        "#rect()",
        "a word",
        ", a: 1",
        "10.0",
        ":",
        "if i == 0 {true}",
        "for",
        "* hello *",
        "//",
        "/*",
        "\\u{12e4}",
        "```typst",
        " ",
        "trees",
        "\\",
        "$ a $",
        "2.",
        "-",
        "5",
    ];

    let mut ok = true;
    let mut apply = |replace: Range<usize>, with| {
        let mut incr_source = Source::detached(text);
        if incr_source.root().len() != text.len() {
            println!(
                "    Subtest {i} tree length {} does not match string length {} ❌",
                incr_source.root().len(),
                text.len(),
            );
            return false;
        }

        incr_source.edit(replace.clone(), with);

        let edited_src = incr_source.text();
        let ref_source = Source::detached(edited_src);
        let ref_root = ref_source.root();
        let incr_root = incr_source.root();

        // Ensures that the span numbering invariants hold.
        let spans_ok = test_spans(output, ref_root) && test_spans(output, incr_root);

        // Ensure that the reference and incremental trees are the same.
        let tree_ok = ref_root.spanless_eq(incr_root);

        if !tree_ok {
            writeln!(
                output,
                "    Subtest {i} reparse differs from clean parse when inserting '{with}' at {}-{} ❌\n",
                replace.start, replace.end,
            ).unwrap();
            writeln!(output, "    Expected reference tree:\n{ref_root:#?}\n").unwrap();
            writeln!(output, "    Found incremental tree:\n{incr_root:#?}").unwrap();
            writeln!(
                output,
                "    Full source ({}):\n\"{edited_src:?}\"",
                edited_src.len()
            )
            .unwrap();
        }

        spans_ok && tree_ok
    };

    let mut pick = |range: Range<usize>| {
        let ratio = rng.next();
        (range.start as f64 + ratio * (range.end - range.start) as f64).floor() as usize
    };

    let insertions = (text.len() as f64 / 400.0).ceil() as usize;
    for _ in 0..insertions {
        let supplement = supplements[pick(0..supplements.len())];
        let start = pick(0..text.len());
        let end = pick(start..text.len());

        if !text.is_char_boundary(start) || !text.is_char_boundary(end) {
            continue;
        }

        ok &= apply(start..end, supplement);
    }

    let source = Source::detached(text);
    let leafs = leafs(source.root());
    let start = source.find(leafs[pick(0..leafs.len())].span()).unwrap().offset();
    let supplement = supplements[pick(0..supplements.len())];
    ok &= apply(start..start, supplement);

    ok
}

/// Returns all leaf descendants of a node (may include itself).
fn leafs(node: &SyntaxNode) -> Vec<SyntaxNode> {
    if node.children().len() == 0 {
        vec![node.clone()]
    } else {
        node.children().flat_map(leafs).collect()
    }
}

/// Ensure that all spans are properly ordered (and therefore unique).
#[track_caller]
fn test_spans(output: &mut String, root: &SyntaxNode) -> bool {
    test_spans_impl(output, root, 0..u64::MAX)
}

#[track_caller]
fn test_spans_impl(output: &mut String, node: &SyntaxNode, within: Range<u64>) -> bool {
    if !within.contains(&node.span().number()) {
        writeln!(output, "    Node: {node:#?}").unwrap();
        writeln!(
            output,
            "    Wrong span order: {} not in {within:?} ❌",
            node.span().number()
        )
        .unwrap();
    }

    let start = node.span().number() + 1;
    let mut children = node.children().peekable();
    while let Some(child) = children.next() {
        let end = children.peek().map_or(within.end, |next| next.span().number());
        if !test_spans_impl(output, child, start..end) {
            return false;
        }
    }

    true
}

/// Draw all frames into one image with padding in between.
fn render(document: &Document) -> sk::Pixmap {
    let pixel_per_pt = 2.0;
    let padding = Abs::pt(5.0);

    for page in &document.pages {
        let limit = Abs::cm(100.0);
        if page.frame.width() > limit || page.frame.height() > limit {
            panic!("overlarge frame: {:?}", page.frame.size());
        }
    }

    let mut pixmap = typst_render::render_merged(
        document,
        pixel_per_pt,
        Color::WHITE,
        padding,
        Color::BLACK,
    );

    let padding = (pixel_per_pt * padding.to_pt() as f32).round();
    let [x, mut y] = [padding; 2];
    for page in &document.pages {
        let ts =
            sk::Transform::from_scale(pixel_per_pt, pixel_per_pt).post_translate(x, y);
        render_links(&mut pixmap, ts, &page.frame);
        y += (pixel_per_pt * page.frame.height().to_pt() as f32).round().max(1.0)
            + padding;
    }

    pixmap
}

/// Draw extra boxes for links so we can see whether they are there.
fn render_links(canvas: &mut sk::Pixmap, ts: sk::Transform, frame: &Frame) {
    for (pos, item) in frame.items() {
        let ts = ts.pre_translate(pos.x.to_pt() as f32, pos.y.to_pt() as f32);
        match *item {
            FrameItem::Group(ref group) => {
                let ts = ts.pre_concat(to_sk_transform(&group.transform));
                render_links(canvas, ts, &group.frame);
            }
            FrameItem::Meta(Meta::Link(_), size) => {
                let w = size.x.to_pt() as f32;
                let h = size.y.to_pt() as f32;
                let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
                let mut paint = sk::Paint::default();
                paint.set_color_rgba8(40, 54, 99, 40);
                canvas.fill_rect(rect, &paint, ts, None);
            }
            _ => {}
        }
    }
}

fn to_sk_transform(transform: &Transform) -> sk::Transform {
    let Transform { sx, ky, kx, sy, tx, ty } = *transform;
    sk::Transform::from_row(
        sx.get() as _,
        ky.get() as _,
        kx.get() as _,
        sy.get() as _,
        tx.to_pt() as f32,
        ty.to_pt() as f32,
    )
}

/// A Linear-feedback shift register using XOR as its shifting function.
/// Can be used as PRNG.
struct LinearShift(u64);

impl LinearShift {
    /// Initialize the shift register with a pre-set seed.
    pub fn new() -> Self {
        Self(0xACE5)
    }

    /// Return a pseudo-random number between `0.0` and `1.0`.
    pub fn next(&mut self) -> f64 {
        self.0 ^= self.0 >> 3;
        self.0 ^= self.0 << 14;
        self.0 ^= self.0 >> 28;
        self.0 ^= self.0 << 36;
        self.0 ^= self.0 >> 52;
        self.0 as f64 / u64::MAX as f64
    }
}
