use anyhow::{Context, Result};
use regex::Regex;
use std::io::prelude::*;
use std::io::{BufRead, BufReader};
use std::str::FromStr;
use std::time::{Duration, Instant};
use timeout_readwrite::TimeoutReader;

// See s_qpTestResultMap in qpTestLog.c
#[derive(Debug, PartialEq)]
pub enum DeqpStatus {
    Pass,
    Fail,
    QualityWarning,
    CompatibilityWarning,
    Pending,
    NotSupported,
    ResourceError,
    InternalError,
    Crash,
    Timeout,
    Waiver,
}

impl FromStr for DeqpStatus {
    type Err = anyhow::Error;

    // Parses the status name from dEQP's output.
    fn from_str(input: &str) -> Result<DeqpStatus, Self::Err> {
        match input {
            "Pass" => Ok(DeqpStatus::Pass),
            "Fail" => Ok(DeqpStatus::Fail),
            "QualityWarning" => Ok(DeqpStatus::QualityWarning),
            "CompatibilityWarning" => Ok(DeqpStatus::CompatibilityWarning),
            "Pending" => Ok(DeqpStatus::Pending),
            "NotSupported" => Ok(DeqpStatus::NotSupported),
            "ResourceError" => Ok(DeqpStatus::ResourceError),
            "InternalError" => Ok(DeqpStatus::InternalError),
            "Crash" => Ok(DeqpStatus::Crash),
            "Timeout" => Ok(DeqpStatus::Timeout),
            "Waiver" => Ok(DeqpStatus::Waiver),
            _ => anyhow::bail!("unknown dEQP status '{}'", input),
        }
    }
}

#[derive(Debug)]
pub struct DeqpTestResult {
    pub name: String,
    pub status: DeqpStatus,
    pub duration: Duration,
}

// For comparing equality, we ignore the test runtime (particularly of use for the unit tests )
impl PartialEq for DeqpTestResult {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name && self.status == other.status
    }
}

pub fn parse_deqp_results<B>(deqp_output: B) -> Result<Vec<DeqpTestResult>>
where
    B: Read,
{
    let deqp_output = BufReader::new(deqp_output);
    let mut current_test: Option<(String, Instant)> = None;
    lazy_static! {
        static ref TEST_RE: Regex = Regex::new("Test case '(.*)'..").unwrap();
        static ref STATUS_RE: Regex = Regex::new("^  (\\S*) \\(.*\\)").unwrap();
    }

    let mut results = Vec::new();

    for line in deqp_output.lines() {
        let line = match line {
            Ok(line) => line,
            Err(e) => {
                if let std::io::ErrorKind::TimedOut = e.kind() {
                    if let Some((ref name, time)) = current_test {
                        results.push(DeqpTestResult {
                            name: name.to_string(),
                            status: DeqpStatus::Timeout,
                            duration: time.elapsed(),
                        });
                        return Ok(results);
                    }
                }

                return Err(e).context("Reading from dEQP");
            }
        };

        if let Some((ref name, time)) = current_test {
            if let Some(cap) = STATUS_RE.captures(&line) {
                let status = DeqpStatus::from_str(&cap[1])?;

                results.push(DeqpTestResult {
                    name: name.to_string(),
                    status,
                    duration: time.elapsed(),
                });
                current_test = None;
            }
        } else if let Some(cap) = TEST_RE.captures(&line) {
            current_test = Some((cap[1].to_string(), Instant::now()));
        }
    }
    if let Some((ref name, time)) = current_test {
        results.push(DeqpTestResult {
            name: name.clone(),
            status: DeqpStatus::Crash,
            duration: time.elapsed(),
        });
    }

    Ok(results)
}

pub fn parse_deqp_results_with_timeout<B>(
    deqp_output: B,
    timeout: Duration,
) -> Result<Vec<DeqpTestResult>>
where
    B: Read + std::os::unix::io::AsRawFd,
{
    parse_deqp_results(TimeoutReader::new(deqp_output, timeout))
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::process::{Command, Stdio};

    fn result(name: &str, status: DeqpStatus) -> DeqpTestResult {
        DeqpTestResult {
            name: name.to_string(),
            status,
            duration: Duration::new(0, 0),
        }
    }

    #[test]
    fn parse_statuses() {
        let output = "
Writing test log into /home/anholt/TestResults.qpa
dEQP Core git-f442587f49bded2eec0c04fca6eb78d60ee83e8f (0xf442587f) starting..
  target implementation = 'Surfaceless'

Test case 'dEQP-GLES2.test.p'..
  Pass (Supported)

Test case 'dEQP-GLES2.test.f'..
  Fail (Found invalid pixel values)

Test case 'dEQP-GLES2.test.q'..
  QualityWarning (not so hot)

Test case 'dEQP-GLES2.test.c'..
  CompatibilityWarning (some bug)

Test case 'dEQP-GLES2.test.pend'..
  Pending (is there even a test that emits this)

Test case 'dEQP-GLES2.test.ns'..
  NotSupported (GL_EXT_tessellation_shader is not supported)

Test case 'dEQP-GLES2.test.re'..
  ResourceError (something missing)

Test case 'dEQP-GLES2.test.ie'..
  InternalError (whoops)

Test case 'dEQP-GLES2.test.cr'..
  Crash (boom)

Test case 'dEQP-GLES2.test.time'..
  Timeout (deqp watchdog)

Test case 'dEQP-GLES2.test.waive'..
  Waiver (it's fine)

DONE!

Test run totals:
  Passed:        6/6 (100.0%)
  Failed:        0/6 (0.0%)
  Not supported: 0/6 (0.0%)
  Warnings:      0/6 (0.0%)
  Waived:        0/6 (0.0%)";

        assert_eq!(
            parse_deqp_results(&mut output.as_bytes()).unwrap(),
            vec![
                result("dEQP-GLES2.test.p", DeqpStatus::Pass),
                result("dEQP-GLES2.test.f", DeqpStatus::Fail),
                result("dEQP-GLES2.test.q", DeqpStatus::QualityWarning),
                result("dEQP-GLES2.test.c", DeqpStatus::CompatibilityWarning),
                result("dEQP-GLES2.test.pend", DeqpStatus::Pending),
                result("dEQP-GLES2.test.ns", DeqpStatus::NotSupported),
                result("dEQP-GLES2.test.re", DeqpStatus::ResourceError),
                result("dEQP-GLES2.test.ie", DeqpStatus::InternalError),
                result("dEQP-GLES2.test.cr", DeqpStatus::Crash),
                result("dEQP-GLES2.test.time", DeqpStatus::Timeout),
                result("dEQP-GLES2.test.waive", DeqpStatus::Waiver),
            ]
        );
    }

    #[test]
    /// Test parsing a run that didn't produce any results, common when doing something like
    /// using a test list for the wrong deqp binary.
    fn parse_all_missing() {
        let output = "
dEQP Core git-f442587f49bded2eec0c04fca6eb78d60ee83e8f (0xf442587f) starting..
  target implementation = 'Surfaceless'

DONE!

Test run totals:
  Passed:        0/0 (0.0%)
  Failed:        0/0 (0.0%)
  Not supported: 0/0 (0.0%)
  Warnings:      0/0 (0.0%)
  Waived:        0/0 (0.0%)";

        assert_eq!(
            parse_deqp_results(&mut output.as_bytes()).unwrap(),
            Vec::new()
        );
    }
    #[test]
    fn parse_crash() {
        let output = "
Writing test log into /home/anholt/TestResults.qpa
dEQP Core git-f442587f49bded2eec0c04fca6eb78d60ee83e8f (0xf442587f) starting..
  target implementation = 'Surfaceless'

Test case 'dEQP-GLES2.test.p'..
  Pass (Supported)

Test case 'dEQP-GLES2.test.c'..";

        assert_eq!(
            parse_deqp_results(&mut output.as_bytes()).unwrap(),
            vec![
                result("dEQP-GLES2.test.p", DeqpStatus::Pass),
                result("dEQP-GLES2.test.c", DeqpStatus::Crash),
            ]
        );
    }

    #[test]
    fn parse_failure() {
        let output = "
Writing test log into /home/anholt/TestResults.qpa
dEQP Core git-f442587f49bded2eec0c04fca6eb78d60ee83e8f (0xf442587f) starting..
  target implementation = 'Surfaceless'

Test case 'dEQP-GLES2.test.p'..
  Pass (Supported)

Test case 'dEQP-GLES2.test.parsefail'..
  UnknownStatus (unknown)";

        assert!(parse_deqp_results(&mut output.as_bytes()).is_err());
    }

    #[test]
    fn parse_parens_in_detail() {
        let output = "
Writing test log into /home/anholt/TestResults.qpa
dEQP Core git-f442587f49bded2eec0c04fca6eb78d60ee83e8f (0xf442587f) starting..
  target implementation = 'Surfaceless'

Test case 'dEQP-GLES2.test.parens'..
  NotSupported (Test requires GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS (0) >= 0)'..";

        assert_eq!(
            parse_deqp_results(&mut output.as_bytes()).unwrap(),
            vec![result("dEQP-GLES2.test.parens", DeqpStatus::NotSupported),]
        );
    }

    #[test]
    fn parse_timeout() -> Result<()> {
        // The spawning here mimics DeqpCommand's, since we want to make sure that it
        // will handle timeouts well, but we don't really want all of its deqp temp files
        // handling for this test.
        let mut child = Command::new("cat")
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .unwrap();

        let mut stdin = std::io::BufWriter::new(child.stdin.take().unwrap());

        stdin
            .write_all(
                "
dEQP Core git-f442587f49bded2eec0c04fca6eb78d60ee83e8f (0xf442587f) starting..
    target implementation = 'Surfaceless'
Test case 'dEQP-GLES2.test.p'..
  Pass (Supported)

Test case 'dEQP-GLES2.test.t'..
"
                .as_bytes(),
            )
            .unwrap();
        stdin.flush().unwrap();

        let stdout = child.stdout.take().context("opening stdout")?;

        assert_eq!(
            parse_deqp_results_with_timeout(stdout, Duration::new(0, 100_000_000))
                .context("parsing results")
                .unwrap(),
            vec![
                result("dEQP-GLES2.test.p", DeqpStatus::Pass),
                result("dEQP-GLES2.test.t", DeqpStatus::Timeout),
            ]
        );

        child.kill().context("killing cat")
    }

    #[test]
    fn parse_turnip_stdout_mixup() -> Result<()> {
        let output = include_bytes!("../resources/test/turnip-stdout-mixup.txt");
        let results = parse_deqp_results(&mut output.as_ref())?;

        for result in &results {
            // Make sure parsing didn't mix up our stdout with some other lines.
            assert!(result.name.starts_with("dEQP-VK."));
        }

        assert_eq!(
            results[results.len() - 1].name,
            "dEQP-VK.compute.device_group.device_index"
        );
        assert_eq!(results[results.len() - 1].status, DeqpStatus::Crash);

        assert_eq!(results.len(), 347);

        Ok(())
    }
}
