//! Fuzzy testing.
//! Tries to discover panics with random bytes.
//! This test is expensive and therefore marked with `#[ignore]`. To run this
//! test, use `cargo test -- --ignored`.

use std::panic::catch_unwind;

use rand::{rngs::StdRng, Rng};

extern crate exr;
use std::{
    ffi::OsStr,
    fs::File,
    io::{Cursor, Write},
    path::PathBuf,
};

use exr::{
    image::{pixel_vec::PixelVec, read::read_first_rgba_layer_from_file},
    prelude::*,
};

fn exr_files(path: &'static str, filter: bool) -> impl Iterator<Item = PathBuf> {
    walkdir::WalkDir::new(path)
        .into_iter()
        .map(std::result::Result::unwrap)
        .filter(|entry| entry.path().is_file())
        .filter(move |entry| !filter || entry.path().extension() == Some(OsStr::new("exr")))
        .map(walkdir::DirEntry::into_path)
}

/// Just don't panic.
#[test]
pub fn fuzzed() {
    for ref file in exr_files("tests/images/fuzzed", false) {
        let _ = read()
            .no_deep_data()
            .largest_resolution_level()
            .all_channels()
            .first_valid_layer()
            .all_attributes()
            .pedantic()
            .from_file(file);

        let _ = read()
            .no_deep_data()
            .all_resolution_levels()
            .all_channels()
            .all_layers()
            .all_attributes()
            .pedantic()
            .from_file(file);
    }
}

/// Require an error but no panic.
#[test]
pub fn damaged() {
    let mut passed = true;

    for ref file in exr_files("tests/images/invalid", false) {
        let result = catch_unwind(move || {
            let _meta_data = MetaData::read_from_file(file, false)?;

            {
                let _minimal = read()
                    .no_deep_data()
                    .largest_resolution_level()
                    .rgba_channels(
                        |_size, _channels| (),
                        |_: &mut (),
                         _position: Vec2<usize>,
                         _pixel: (Sample, Sample, Sample, Sample)| {},
                    )
                    .first_valid_layer()
                    .all_attributes()
                    .from_file(file)?;
            }

            {
                let _minimal = read()
                    .no_deep_data()
                    .largest_resolution_level() // TODO all levels
                    .rgba_channels(
                        |_size, _channels| (),
                        |_: &mut (),
                         _position: Vec2<usize>,
                         _pixel: (Sample, Sample, Sample, Sample)| {},
                    )
                    .all_layers()
                    .all_attributes()
                    .pedantic()
                    .from_file(file)?;
            }

            {
                let _rgba = read_first_rgba_layer_from_file(
                    file,
                    PixelVec::<(Sample, Sample, Sample, Sample)>::constructor,
                    PixelVec::set_pixel,
                )?;
            }

            {
                let _full = read_all_data_from_file(file)?;
            }

            Ok(())
        });

        // this should not panic, only err:
        passed = passed
            && match result {
                Ok(Err(Error::Invalid(message))) => {
                    println!("✓ Recognized as invalid ({}): {:?}", message, file);
                    true
                }

                Ok(Err(Error::NotSupported(message))) => {
                    println!("- Unsupported ({}): {:?}", message, file);
                    true
                }

                Ok(Err(Error::Io(error))) => {
                    println!("✗ Unexpected IO Error: {:?}, {:?}", file, error);
                    false
                }

                Err(_) => {
                    println!("✗ Not recognized as invalid: {:?}", file);
                    false
                }

                Ok(Ok(_)) => {
                    let meta_data = MetaData::read_from_file(file, true);

                    if let Err(error) = meta_data {
                        println!("✓ Recognized as invalid when pedantic ({}): {:?}", error, file);
                        true
                    } else {
                        println!("✗ Oh no, there is nothing wrong with: {:#?}", file);
                        false
                    }
                }

                _ => unreachable!(),
            };
    }

    assert!(passed, "A damaged file was not handled correctly");
}

#[test]
#[ignore]
pub fn fuzz() {
    println!("started fuzzing");
    let files: Vec<PathBuf> = exr_files("tests/images", true).collect();

    let seed = [
        92, 1, 0, 130, 211, 8, 21, 70, 74, 4, 9, 5, 0, 23, 0, 3, 20, 25, 6, 5, 229, 30, 0, 34, 218,
        0, 40, 7, 5, 2, 7, 0,
    ];
    let mut random: StdRng = rand::SeedableRng::from_seed(seed);

    let mut records = File::create("tests/images/fuzzed/list.txt").unwrap();
    records.write_all(format!("seed = {:?}", seed).as_bytes()).unwrap();

    let start_index = 0; // default is 0. increase this integer for debugging a specific fuzz case
    for fuzz_index in 0..1024_u64 * 2048 * 4 {
        let file_1_name = &files[random.gen_range(0..files.len())];
        let mutation_point = random.gen::<f32>().powi(3);
        let mutation = random.gen::<u8>();

        if fuzz_index >= start_index {
            let mut file = std::fs::read(file_1_name).unwrap();
            let index = ((mutation_point * file.len() as f32) as usize + 4) % file.len();
            file[index] = mutation;

            let file = file.as_slice();
            let result = catch_unwind(move || {
                let read_all_data = read()
                    .no_deep_data()
                    .all_resolution_levels()
                    .all_channels()
                    .all_layers()
                    .all_attributes();

                match read_all_data.from_buffered(Cursor::new(file)) {
                    Err(Error::Invalid(error)) => {
                        println!("✓ No Panic. [{}]: Invalid: {}.", fuzz_index, error)
                    }
                    Err(Error::NotSupported(error)) => {
                        println!("- No Panic. [{}]: Unsupported: {}.", fuzz_index, error)
                    }
                    _ => {}
                }
            });

            if let Err(_) = result {
                records.write_all(fuzz_index.to_string().as_bytes()).unwrap();
                records.flush().unwrap();

                let seed =
                    seed.iter().map(|num| num.to_string()).collect::<Vec<String>>().join("-");
                let mut saved =
                    File::create(format!("tests/images/fuzzed/fuzz_{}_{}.exr", fuzz_index, seed))
                        .unwrap();
                saved.write_all(file).unwrap();

                println!("✗ PANIC! [{}]", fuzz_index);
            }
        }
    }
}
