//! Shared deterministic fixtures used by translated compatibility tests.

pub mod rlv_support;

use std::collections::BTreeMap;
use std::future::Future;
use std::io;
use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Wake, Waker};
use std::time::Duration;

/// Runs one test future without choosing an async runtime for the library.
pub fn block_on<F: Future>(future: F) -> F::Output {
    struct ThreadWake(std::thread::Thread);

    impl Wake for ThreadWake {
        fn wake(self: Arc<Self>) {
            self.0.unpark();
        }
    }

    let waker = Waker::from(Arc::new(ThreadWake(std::thread::current())));
    let mut context = Context::from_waker(&waker);
    let mut future = std::pin::pin!(future);
    loop {
        match future.as_mut().poll(&mut context) {
            Poll::Ready(output) => return output,
            Poll::Pending => std::thread::park(),
        }
    }
}

/// Loads the live-grid identity from the process environment or workspace `.env`.
///
/// # Panics
///
/// Panics when one of `GRID_USER`, `GRID_PASSWORD`, or `GRID_LOGIN_URL` is absent.
#[must_use]
pub fn live_grid_credentials() -> (String, String, String, String) {
    fn credential(name: &str) -> String {
        if let Ok(value) = std::env::var(name)
            && !value.trim().is_empty()
        {
            return value;
        }
        let dotenv = Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../..")
            .join(".env");
        let contents = std::fs::read_to_string(dotenv).expect("read workspace .env");
        contents
            .lines()
            .find_map(|line| {
                let line = line.trim().strip_prefix("export ").unwrap_or(line.trim());
                let (key, value) = line.split_once('=')?;
                (key.trim() == name).then(|| value.trim().trim_matches(['\'', '"']).to_owned())
            })
            .filter(|value| !value.is_empty())
            .unwrap_or_else(|| panic!("live test requires {name}"))
    }

    let full_name = credential("GRID_USER");
    let password = credential("GRID_PASSWORD");
    let login_url = credential("GRID_LOGIN_URL");
    let mut names = full_name.split_whitespace();
    let first = names.next().expect("live first name").to_owned();
    let last = names.next().expect("live last name").to_owned();
    (first, last, password, login_url)
}

/// Asserts equality within the exact absolute tolerance carried by an upstream test.
///
/// # Panics
///
/// Panics when the tolerance is invalid or the values differ beyond it.
#[track_caller]
pub fn assert_close(actual: f64, expected: f64, tolerance: f64) {
    assert!(
        tolerance.is_finite() && tolerance >= 0.0,
        "tolerance must be finite and non-negative"
    );
    assert!(
        (actual - expected).abs() <= tolerance,
        "expected {expected:?} +/- {tolerance:?}, got {actual:?}"
    );
}

/// Asserts byte equality and reports the first differing offset.
///
/// # Panics
///
/// Panics when the byte slices differ.
#[track_caller]
pub fn assert_bytes_eq(actual: &[u8], expected: &[u8]) {
    if actual == expected {
        return;
    }
    let offset = actual
        .iter()
        .zip(expected)
        .position(|(actual, expected)| actual != expected)
        .unwrap_or_else(|| actual.len().min(expected.len()));
    panic!(
        "byte fixtures differ at offset {offset}: expected {} bytes, got {} bytes",
        expected.len(),
        actual.len()
    );
}

/// Decodes a whitespace-separated or contiguous hexadecimal byte fixture.
///
/// # Errors
///
/// Returns an error for an odd digit count or a non-hexadecimal digit.
pub fn decode_hex(input: &str) -> Result<Vec<u8>, String> {
    let digits: Vec<_> = input
        .bytes()
        .filter(|byte| !byte.is_ascii_whitespace())
        .collect();
    if digits.len() % 2 != 0 {
        return Err("hex fixture has an odd number of digits".into());
    }
    digits
        .chunks_exact(2)
        .enumerate()
        .map(|(index, pair)| {
            let high = hex_digit(pair[0]);
            let low = hex_digit(pair[1]);
            high.zip(low)
                .map(|(high, low)| high << 4 | low)
                .ok_or_else(|| format!("invalid hex byte at digit {}", index * 2))
        })
        .collect()
}

fn hex_digit(byte: u8) -> Option<u8> {
    match byte {
        b'0'..=b'9' => Some(byte - b'0'),
        b'a'..=b'f' => Some(byte - b'a' + 10),
        b'A'..=b'F' => Some(byte - b'A' + 10),
        _ => None,
    }
}

/// Resolves test data below a fixture root without permitting path traversal.
///
/// # Errors
///
/// Returns [`io::ErrorKind::InvalidInput`] for absolute or parent-relative paths.
pub fn test_data_path(root: &Path, relative: &Path) -> io::Result<PathBuf> {
    if relative.components().any(|component| {
        matches!(
            component,
            Component::ParentDir | Component::RootDir | Component::Prefix(_)
        )
    }) {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "test data path must stay below its fixture root",
        ));
    }
    Ok(root.join(relative))
}

/// Loads a test-data file through [`test_data_path`].
///
/// # Errors
///
/// Returns path validation and file-read errors.
pub fn load_test_data(root: &Path, relative: &Path) -> io::Result<Vec<u8>> {
    std::fs::read(test_data_path(root, relative)?)
}

/// A cloneable clock advanced only by the test.
#[derive(Clone, Debug, Default)]
pub struct ManualClock(Arc<Mutex<Duration>>);

impl ManualClock {
    /// Returns the deterministic elapsed time.
    #[must_use]
    pub fn now(&self) -> Duration {
        *self
            .0
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    /// Advances the clock and returns its new value.
    ///
    /// # Panics
    ///
    /// Panics if the resulting duration exceeds [`Duration::MAX`].
    #[must_use]
    pub fn advance(&self, duration: Duration) -> Duration {
        let mut now = self
            .0
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        *now += duration;
        *now
    }
}

/// A recorded deterministic HTTP-like request.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RecordedRequest {
    pub method: String,
    pub uri: String,
    pub body: Vec<u8>,
}

/// A canned deterministic HTTP-like response.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FakeResponse {
    pub status: u16,
    pub content_type: String,
    pub body: Vec<u8>,
}

/// An in-memory exact-URI/path fake with ordered request capture.
#[derive(Debug, Default)]
pub struct FakeNetwork {
    exact: Mutex<BTreeMap<(String, String), FakeResponse>>,
    paths: Mutex<BTreeMap<(String, String), FakeResponse>>,
    requests: Mutex<Vec<RecordedRequest>>,
}

impl FakeNetwork {
    /// Adds an exact method/URI response.
    pub fn add_response(&self, method: &str, uri: &str, response: FakeResponse) {
        self.exact
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .insert((method.to_owned(), uri.to_owned()), response);
    }

    /// Adds a response matched after removing the request query string.
    pub fn add_path_response(&self, method: &str, uri_without_query: &str, response: FakeResponse) {
        self.paths
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .insert((method.to_owned(), uri_without_query.to_owned()), response);
    }

    /// Records a request and returns its configured response, or a deterministic 404.
    pub fn send(&self, method: &str, uri: &str, body: impl Into<Vec<u8>>) -> FakeResponse {
        self.requests
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .push(RecordedRequest {
                method: method.to_owned(),
                uri: uri.to_owned(),
                body: body.into(),
            });
        let key = (method.to_owned(), uri.to_owned());
        if let Some(response) = self
            .exact
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .get(&key)
        {
            return response.clone();
        }
        let path_key = (
            method.to_owned(),
            uri.split('?').next().unwrap_or(uri).to_owned(),
        );
        self.paths
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .get(&path_key)
            .cloned()
            .unwrap_or_else(|| FakeResponse {
                status: 404,
                content_type: "application/octet-stream".into(),
                body: Vec::new(),
            })
    }

    /// Returns all captured requests in send order.
    #[must_use]
    pub fn requests(&self) -> Vec<RecordedRequest> {
        self.requests
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
            .clone()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn deterministic_harness_covers_clock_network_and_bytes() {
        assert_eq!(block_on(std::future::ready(7)), 7);
        assert_close(1.001, 1.0, 0.01);
        assert_bytes_eq(&decode_hex("00 ff 2A").unwrap(), &[0, 255, 42]);
        assert!(decode_hex("é").is_err());

        let clock = ManualClock::default();
        assert_eq!(
            clock.advance(Duration::from_millis(25)),
            Duration::from_millis(25)
        );
        assert_eq!(clock.now(), Duration::from_millis(25));

        let network = FakeNetwork::default();
        network.add_path_response(
            "GET",
            "https://example.test/cap",
            FakeResponse {
                status: 200,
                content_type: "application/llsd+xml".into(),
                body: b"fixture".to_vec(),
            },
        );
        assert_eq!(
            network.send("GET", "https://example.test/cap?tid=1", []),
            FakeResponse {
                status: 200,
                content_type: "application/llsd+xml".into(),
                body: b"fixture".to_vec(),
            }
        );
        assert_eq!(network.requests()[0].uri, "https://example.test/cap?tid=1");
    }

    #[test]
    fn test_data_rejects_parent_traversal() {
        assert_eq!(
            test_data_path(Path::new("fixtures"), Path::new("../secret"))
                .unwrap_err()
                .kind(),
            io::ErrorKind::InvalidInput
        );
    }
}
