//! Mock trace tool for snapshot and performance integration testing without
//! real replay tools.
//!
//! Recognized by the `.mock-trace` file extension, where the file format is
//! just a .png.  Snapshot mode generates frames derived from the trace's image
//! data, and replay mode echoes a fake FPS line so that wrapper scripts created
//! by `env_reproducible_outputs` can substitute per-sample values via their sed
//! pipeline.
//!
//! Environment variables (read by gpu-trace-perf, not by an external binary):
//!
//! * `MOCK_BREAK_RENDERING` – if set, replaces every transparent-black pixel
//!   (`[0,0,0,x]`) with `[0xd0,0xd0,0xd0,0xd0]` in all snapshot frames
//! * `MOCK_FLAKE_RENDERING` – same replacement, but only on frame 3
//! * `MOCK_TIMEOUT` – if set, runs `sleep 60` instead of producing frames,
//!   so that snapshot timeout tests can verify the kill path with a short timeout

use crate::relative_test_name;
use crate::{ReplayOutput, TraceTool, replay_command, snapshot::SnapshotResult};
use anyhow::{Context, Result};
use image::{DynamicImage, Rgba};
use std::io::{Read, Write};
use std::path::Path;
use std::time::Duration;
use std::{path::PathBuf, time::Instant};

pub struct MockTrace {
    file: PathBuf,
    name: String,
}

impl MockTrace {
    pub fn new(root: &Path, file: &Path) -> MockTrace {
        MockTrace {
            file: file.to_owned(),
            name: relative_test_name(root, file),
        }
    }
}

impl TraceTool for MockTrace {
    fn replay(&self, wrapper: Option<&str>, envs: &[(String, String)]) -> Result<ReplayOutput> {
        // Use `echo` as the underlying "replay command".  With no wrapper this
        // just returns the fps_line straight; with env_reproducible_outputs the
        // wrapper's sed replaces the fps value with $sample.
        Ok(self.run_replay_command(replay_command(
            &["echo", "Measured FPS: 10.0 fps,"],
            wrapper,
            envs,
        )))
    }

    fn fps(&self, output: &ReplayOutput) -> Result<f64> {
        for line in output.stdout.lines() {
            if let Some(rest) = line.strip_prefix("Measured FPS: ") {
                if let Some(fps_str) = rest.split_whitespace().next() {
                    return fps_str.parse::<f64>().context("parsing mock FPS value");
                }
            }
        }
        anyhow::bail!("No 'Measured FPS:' line found in mock trace output")
    }

    fn name(&self) -> &str {
        &self.name
    }

    fn can_snapshot(&self) -> bool {
        true
    }

    fn snapshot(&self, output_dir: &str, loops: u32, timeout: Duration) -> Result<SnapshotResult> {
        let output_dir_path = self.output_dir(output_dir)?.unwrap();

        if std::env::var("MOCK_TIMEOUT").is_ok() {
            let start = Instant::now();
            let mut cmd = std::process::Command::new("sleep");
            cmd.arg("60");
            let output = self.run_replay_command_with_timeout(cmd, None, timeout);
            return Ok(SnapshotResult {
                files: vec![],
                output,
                runtime: start.elapsed(),
            });
        }

        let break_rendering = std::env::var("MOCK_BREAK_RENDERING").is_ok();
        let flake_rendering = std::env::var("MOCK_FLAKE_RENDERING").is_ok();

        let modified = if break_rendering || flake_rendering {
            let mut raw = Vec::new();
            std::fs::File::open(&self.file)
                .with_context(|| format!("opening {:?}", &self.file))?
                .read_to_end(&mut raw)
                .with_context(|| format!("reading {:?}", &self.file))?;
            let image = image::load_from_memory(&raw)
                .with_context(|| format!("loading mock trace file {:?} as an image", &self.file))?;

            let modified = modify_image(&image);
            let mut out: Vec<u8> = Vec::new();
            modified
                .write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png)
                .context("compressing modified png")?;

            Some(out)
        } else {
            None
        };

        let start = Instant::now();
        let mut files = Vec::new();

        for frame in 1..=loops as usize {
            let should_modify = break_rendering || (flake_rendering && frame == 3);

            let filename = format!("snapshot{frame:04}.png");
            let path = output_dir_path.join(&filename);

            if should_modify {
                std::fs::File::create(path)
                    .context("creating {path:?}")?
                    .write_all(modified.as_ref().unwrap())
                    .context("writing modified png to {path:?}")?;
            } else {
                std::fs::copy(&self.file, &path)
                    .with_context(|| format!("copying {:?} to {path:?}", &self.file))?;
            };

            files.push(PathBuf::from(&filename));
        }

        Ok(SnapshotResult {
            files,
            output: ReplayOutput {
                exit_code: 0,
                cmdline: format!("mock-trace-replay {:?}", self.file),
                stdout: "mock-trace-replay stdout log contents".to_string(),
                stderr: "mock-trace-replay stderr log contents".to_string(),
                peak: crate::process_watcher::ProcessPeakMem::default(),
            },
            runtime: start.elapsed(),
        })
    }
}

/// Turns all of the black in the image to gray, to simulate broken rendering.
fn modify_image(image: &DynamicImage) -> DynamicImage {
    let mut rgba = image.to_rgba8();

    for pixel in rgba.pixels_mut() {
        if pixel.0[0..3] == [0u8; 3] {
            *pixel = Rgba([0xd0u8, 0xd0, 0xd0, 0xd0]);
        }
    }

    DynamicImage::ImageRgba8(rgba)
}
