#![forbid(unsafe_code)]

use std::{
    error::Error,
    io::Cursor,
    sync::{Arc, Mutex},
    time::Duration,
};

use ironstorage::{
    presentation::{
        ClipboardBackend, ClipboardContent, ClipboardDisposition, ClipboardError, ClipboardManager,
        ClipboardTimeout, ClipboardWait, DEFAULT_CLIPBOARD_TIMEOUT, QrError, QrMatrix,
    },
    repository::SecretBytes,
};

type TestResult = Result<(), Box<dyn Error>>;

#[derive(Default)]
struct MemoryState {
    text: Option<Vec<u8>>,
    fail_read: bool,
    fail_write: bool,
    fail_clear: bool,
}

#[derive(Clone, Default)]
struct MemoryClipboard(Arc<Mutex<MemoryState>>);

impl MemoryClipboard {
    fn with_text(value: &[u8]) -> Self {
        let backend = Self::default();
        backend.set_text(value);
        backend
    }

    fn set_text(&self, value: &[u8]) {
        let mut state = self.0.lock().expect("test clipboard mutex");
        state.text = Some(value.to_vec());
    }

    fn text(&self) -> Option<Vec<u8>> {
        self.0.lock().expect("test clipboard mutex").text.clone()
    }

    fn fail_cleanup_read(&self) {
        self.0.lock().expect("test clipboard mutex").fail_read = true;
    }
}

impl ClipboardBackend for MemoryClipboard {
    fn read(&mut self) -> Result<ClipboardContent, ClipboardError> {
        let mut state = self.0.lock().map_err(|_| ClipboardError::ReadFailed)?;
        if std::mem::take(&mut state.fail_read) {
            return Err(ClipboardError::ReadFailed);
        }
        match &state.text {
            Some(value) => Ok(ClipboardContent::text(value.clone())),
            None => Ok(ClipboardContent::EmptyOrNonText),
        }
    }

    fn write(&mut self, value: &SecretBytes) -> Result<(), ClipboardError> {
        let mut state = self.0.lock().map_err(|_| ClipboardError::WriteFailed)?;
        if std::mem::take(&mut state.fail_write) {
            return Err(ClipboardError::WriteFailed);
        }
        state.text = Some(value.expose().to_vec());
        Ok(())
    }

    fn clear(&mut self) -> Result<(), ClipboardError> {
        let mut state = self.0.lock().map_err(|_| ClipboardError::WriteFailed)?;
        if std::mem::take(&mut state.fail_clear) {
            return Err(ClipboardError::WriteFailed);
        }
        state.text = None;
        Ok(())
    }
}

#[test]
fn clipboard_timeout_restores_previous_text_and_clears_empty_content() -> TestResult {
    let backend = MemoryClipboard::with_text(b"previous clipboard");
    let observer = backend.clone();
    let mut manager = ClipboardManager::new(backend, ClipboardTimeout::pass_default());
    let secret = SecretBytes::new(b"copied secret".to_vec());
    let disposition = manager.copy_with(&secret, |duration| {
        assert_eq!(duration, DEFAULT_CLIPBOARD_TIMEOUT);
        assert_eq!(
            observer.text().as_deref(),
            Some(b"copied secret".as_slice())
        );
        ClipboardWait::Elapsed
    })?;
    assert_eq!(disposition, ClipboardDisposition::RestoredPrevious);
    assert_eq!(
        observer.text().as_deref(),
        Some(b"previous clipboard".as_slice())
    );

    let backend = MemoryClipboard::default();
    let observer = backend.clone();
    let mut manager = ClipboardManager::new(backend, ClipboardTimeout::pass_default());
    assert_eq!(
        manager.copy_with(&secret, |_| ClipboardWait::Elapsed)?,
        ClipboardDisposition::Cleared
    );
    assert!(observer.text().is_none());
    assert!(!format!("{manager:?}").contains("copied secret"));
    Ok(())
}

#[test]
fn clipboard_races_and_cancellation_never_overwrite_newer_user_content() -> TestResult {
    let backend = MemoryClipboard::with_text(b"previous");
    let observer = backend.clone();
    let mut manager = ClipboardManager::new(backend, ClipboardTimeout::pass_default());
    let secret = SecretBytes::new(b"secret".to_vec());
    assert_eq!(
        manager.copy_with(&secret, |_| {
            observer.set_text(b"new user value");
            ClipboardWait::Elapsed
        })?,
        ClipboardDisposition::PreservedNewer
    );
    assert_eq!(
        observer.text().as_deref(),
        Some(b"new user value".as_slice())
    );

    observer.set_text(b"previous");
    assert_eq!(
        manager.copy_with(&secret, |_| ClipboardWait::Cancelled),
        Err(ClipboardError::Cancelled)
    );
    assert_eq!(observer.text().as_deref(), Some(b"previous".as_slice()));
    Ok(())
}

#[test]
fn clipboard_validation_and_cleanup_failures_are_typed_and_redacted() -> TestResult {
    assert_eq!(
        ClipboardTimeout::new(Duration::ZERO),
        Err(ClipboardError::InvalidTimeout)
    );
    assert_eq!(
        ClipboardTimeout::new(Duration::from_secs(301)),
        Err(ClipboardError::InvalidTimeout)
    );
    let backend = MemoryClipboard::default();
    let observer = backend.clone();
    let mut manager = ClipboardManager::new(backend, ClipboardTimeout::pass_default());
    assert_eq!(
        manager.copy_with(&SecretBytes::new(Vec::new()), |_| ClipboardWait::Elapsed),
        Err(ClipboardError::EmptySecret)
    );
    let secret = SecretBytes::new(b"redacted secret".to_vec());
    let content = ClipboardContent::text(b"redacted previous clipboard".to_vec());
    assert!(!format!("{content:?}").contains("redacted previous clipboard"));
    assert_eq!(
        manager.copy_with(&secret, |_| {
            observer.fail_cleanup_read();
            ClipboardWait::Elapsed
        }),
        Err(ClipboardError::CleanupFailed)
    );
    assert!(
        !ClipboardError::CleanupFailed
            .to_string()
            .contains("redacted secret")
    );
    Ok(())
}

#[test]
fn otp_codes_and_uris_use_the_same_secret_safe_clipboard_lifecycle() -> TestResult {
    for payload in [
        b"287082".as_slice(),
        b"otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example".as_slice(),
    ] {
        let backend = MemoryClipboard::default();
        let observer = backend.clone();
        let mut manager = ClipboardManager::new(backend, ClipboardTimeout::pass_default());
        let secret = SecretBytes::new(payload.to_vec());
        assert_eq!(
            manager.copy_with(&secret, |_| {
                assert_eq!(observer.text().as_deref(), Some(payload));
                ClipboardWait::Elapsed
            })?,
            ClipboardDisposition::Cleared
        );
        assert!(observer.text().is_none());
    }
    Ok(())
}

#[test]
fn qr_matrices_round_trip_and_render_without_plaintext() -> TestResult {
    for payload in [
        "correct horse battery staple",
        "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example",
        "Unicode password: 咖啡☕",
    ] {
        let secret = SecretBytes::new(payload.as_bytes().to_vec());
        let matrix = QrMatrix::encode(&secret)?;
        let simple = rqrr::SimpleGrid::from_func(matrix.width(), |x, y| {
            matrix.is_dark(x, y).expect("coordinates are in range")
        });
        let (_, decoded) = rqrr::Grid::new(simple).decode()?;
        assert_eq!(decoded, payload);
        let terminal = matrix.render_terminal();
        assert!(terminal.expose().ends_with(b"\n"));
        assert!(
            !terminal
                .expose()
                .windows(payload.len())
                .any(|part| part == payload.as_bytes())
        );
        assert!(!format!("{matrix:?}").contains(payload));
    }
    assert!(matches!(
        QrMatrix::encode(&SecretBytes::new(Vec::new())),
        Err(QrError::EmptyPayload)
    ));
    assert!(matches!(
        QrMatrix::encode(&SecretBytes::new(vec![b'x'; 4096])),
        Err(QrError::PayloadTooLarge)
    ));
    Ok(())
}

#[test]
fn qr_image_import_returns_secret_bytes_and_rejects_invalid_input() -> TestResult {
    let payload = SecretBytes::new(
        b"otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example".to_vec(),
    );
    let matrix = QrMatrix::encode(&payload)?;
    let scale = 8_u32;
    let quiet = 4_u32;
    let size = (matrix.width() as u32 + quiet * 2) * scale;
    let mut image = image::GrayImage::from_pixel(size, size, image::Luma([255]));
    for y in 0..matrix.width() {
        for x in 0..matrix.width() {
            if matrix.is_dark(x, y) == Some(true) {
                for dy in 0..scale {
                    for dx in 0..scale {
                        image.put_pixel(
                            (x as u32 + quiet) * scale + dx,
                            (y as u32 + quiet) * scale + dy,
                            image::Luma([0]),
                        );
                    }
                }
            }
        }
    }
    let mut encoded = Cursor::new(Vec::new());
    image.write_to(&mut encoded, image::ImageFormat::Png)?;
    let decoded = QrMatrix::decode_image(&SecretBytes::new(encoded.into_inner()))?;
    assert_eq!(decoded.expose(), payload.expose());
    assert!(matches!(
        QrMatrix::decode_image(&SecretBytes::new(b"not an image".to_vec())),
        Err(QrError::InvalidImage)
    ));
    Ok(())
}
