use std::collections::VecDeque;

use archive_trait::{Archive, Member, MemberMetadata, MemberPayload, SpecialKind};
use thiserror::Error;

const MAX_TEST_CHUNK_BYTES: usize = 64 * 1024;

#[derive(Debug, Error)]
#[error("test archive failure")]
pub struct TestError;

pub type TestEntry = Result<Member<TestPayload>, TestError>;

pub mod entry {
    use super::*;

    pub fn file(path: &str, data: impl Into<Vec<u8>>) -> TestEntry {
        file_with_options(path, data, false, false)
    }

    pub fn invalid_file(path: &str, data: impl Into<Vec<u8>>) -> TestEntry {
        file_with_options(path, data, false, true)
    }

    pub fn executable(path: &str, data: impl Into<Vec<u8>>) -> TestEntry {
        file_with_options(path, data, true, false)
    }

    pub fn reuse_checked_file(path: &str, data: impl Into<Vec<u8>>) -> TestEntry {
        let data = data.into();
        Ok(Member::File {
            metadata: metadata(path),
            size: data.len() as u64,
            executable: false,
            payload: TestPayload::new(data, false).require_buffer_reuse(),
        })
    }

    fn file_with_options(
        path: &str,
        data: impl Into<Vec<u8>>,
        executable: bool,
        fail_at_end: bool,
    ) -> TestEntry {
        let data = data.into();
        Ok(Member::File {
            metadata: metadata(path),
            size: data.len() as u64,
            executable,
            payload: TestPayload::new(data, fail_at_end),
        })
    }

    pub fn directory(path: &str) -> TestEntry {
        Ok(Member::Directory {
            metadata: metadata(path),
        })
    }

    pub fn symbolic_link(path: &str, target: &str) -> TestEntry {
        Ok(Member::SymbolicLink {
            metadata: metadata(path),
            target: target.to_owned(),
        })
    }

    pub fn hard_link(path: &str, target: &str, data: impl Into<Vec<u8>>) -> TestEntry {
        let data = data.into();
        Ok(Member::HardLink {
            metadata: metadata(path),
            target: target.to_owned(),
            size: data.len() as u64,
            payload: TestPayload::new(data, false),
        })
    }

    pub fn special(path: &str, kind: SpecialKind) -> TestEntry {
        Ok(Member::Special {
            metadata: metadata(path),
            kind,
        })
    }

    pub fn error() -> TestEntry {
        Err(TestError)
    }

    fn metadata(path: &str) -> MemberMetadata {
        MemberMetadata {
            path: path.to_owned(),
            position: 0,
        }
    }
}

pub struct TestArchive {
    entries: VecDeque<TestEntry>,
}

impl TestArchive {
    pub fn new(entries: impl IntoIterator<Item = TestEntry>) -> Self {
        Self {
            entries: entries.into_iter().collect(),
        }
    }
}

pub struct TestPayload {
    data: Vec<u8>,
    offset: usize,
    fail_at_end: bool,
    require_buffer_reuse: bool,
}

impl TestPayload {
    fn new(data: Vec<u8>, fail_at_end: bool) -> Self {
        Self {
            data,
            offset: 0,
            fail_at_end,
            require_buffer_reuse: false,
        }
    }

    fn require_buffer_reuse(mut self) -> Self {
        self.require_buffer_reuse = true;
        self
    }
}

impl MemberPayload for TestPayload {
    type Error = TestError;

    async fn next_chunk(
        &mut self,
        buffer: &mut Vec<u8>,
        target_len: usize,
    ) -> Result<bool, Self::Error> {
        if self.require_buffer_reuse && self.offset != 0 && buffer.is_empty() {
            return Err(TestError);
        }
        if self.offset == self.data.len() {
            if self.fail_at_end {
                return Err(TestError);
            }
            return Ok(false);
        }
        buffer.clear();
        let end = self
            .offset
            .saturating_add(target_len.clamp(1, MAX_TEST_CHUNK_BYTES))
            .min(self.data.len());
        buffer.extend_from_slice(&self.data[self.offset..end]);
        self.offset = end;
        Ok(true)
    }

    async fn skip(self) -> Result<(), Self::Error> {
        if self.fail_at_end {
            return Err(TestError);
        }
        Ok(())
    }
}

impl Archive for TestArchive {
    type Error = TestError;
    type Payload<'a> = TestPayload;

    async fn next_member<'a>(
        &'a mut self,
    ) -> Result<Option<Member<Self::Payload<'a>>>, Self::Error> {
        match self.entries.pop_front() {
            Some(entry) => entry.map(Some),
            None => Ok(None),
        }
    }
}
