use crate::Error;
const VERSION: &str = "2.0";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Id {
    pub protoversion: String,
    pub softwareversion: String,
    pub comments: Option<String>,
}
impl Id {
    pub fn v2(softwareversion: impl Into<String>, comments: Option<impl Into<String>>) -> Self {
        Self {
            protoversion: VERSION.into(),
            softwareversion: softwareversion.into(),
            comments: comments.map(Into::into),
        }
    }
    pub fn from_reader<R>(reader: &mut R) -> Result<Self, Error>
    where
        R: std::io::BufRead,
    {
        let text = std::io::BufRead::lines(reader)
            .find(|line| {
                line.as_deref()
                    .map(|line| line.starts_with("SSH"))
                    .unwrap_or(true)
            })
            .ok_or(Error::UnexpectedEof)??;
        text.parse()
    }
    #[cfg(feature = "futures")]
    #[cfg_attr(docsrs, doc(cfg(feature = "futures")))]
    pub async fn from_async_reader<R>(reader: &mut R) -> Result<Self, Error>
    where
        R: futures::io::AsyncBufRead + Unpin,
    {
        use futures::TryStreamExt;
        let text = futures::io::AsyncBufReadExt::lines(reader)
            .try_skip_while(|line| futures::future::ok(!line.starts_with("SSH")))
            .try_next()
            .await?
            .ok_or(Error::UnexpectedEof)?;
        text.parse()
    }
    pub fn to_writer<W>(&self, writer: &mut W) -> Result<(), Error>
    where
        W: std::io::Write,
    {
        writer.write_all(self.to_string().as_bytes())?;
        writer.write_all(b"\r\n")?;
        Ok(())
    }
    #[cfg(feature = "futures")]
    #[cfg_attr(docsrs, doc(cfg(feature = "futures")))]
    pub async fn to_async_writer<W>(&self, writer: &mut W) -> Result<(), Error>
    where
        W: futures::io::AsyncWrite + Unpin,
    {
        use futures::io::AsyncWriteExt;
        writer.write_all(self.to_string().as_bytes()).await?;
        writer.write_all(b"\r\n").await?;
        Ok(())
    }
}
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "SSH-{}-{}", self.protoversion, self.softwareversion)?;
        if let Some(comments) = &self.comments {
            write!(f, " {comments}")?;
        }
        Ok(())
    }
}
impl std::str::FromStr for Id {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (id, comments) = s
            .split_once(' ')
            .map_or_else(|| (s, None), |(id, comments)| (id, Some(comments)));
        match id.splitn(3, '-').collect::<Vec<_>>()[..] {
            ["SSH", protoversion, softwareversion]
                if !protoversion.is_empty() && !softwareversion.is_empty() =>
            {
                Ok(Self {
                    protoversion: protoversion.to_string(),
                    softwareversion: softwareversion.to_string(),
                    comments: comments.map(str::to_string),
                })
            }
            _ => Err(Error::BadIdentifer(s.into())),
        }
    }
}
#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used, clippy::unimplemented)]
    use rstest::rstest;
    use std::str::FromStr;
    use super::*;
    impl PartialEq for Error {
        fn eq(&self, other: &Self) -> bool {
            match (self, other) {
                (Self::Io(l0), Self::Io(r0)) => l0.kind() == r0.kind(),
                _ => core::mem::discriminant(self) == core::mem::discriminant(other),
            }
        }
    }
    #[rstest]
    #[case("SSH-2.0-billsSSH_3.6.3q3")]
    #[case("SSH-1.99-billsSSH_3.6.3q3")]
    #[case("SSH-2.0-billsSSH_3.6.3q3 with-comment")]
    #[case("SSH-2.0-billsSSH_3.6.3q3 utf∞-comment")]
    #[case("SSH-2.0-billsSSH_3.6.3q3 ")] fn it_parses_valid(#[case] text: &str) {
        Id::from_str(text).expect(text);
    }
    #[rstest]
    #[case("")]
    #[case("FOO-2.0-billsSSH_3.6.3q3")]
    #[case("-2.0-billsSSH_3.6.3q3")]
    #[case("SSH--billsSSH_3.6.3q3")]
    #[case("SSH-2.0-")]
    fn it_rejects_invalid(#[case] text: &str) {
        Id::from_str(text).expect_err(text);
    }
    #[rstest]
    #[case(Id::v2("billsSSH_3.6.3q3", None::<String>))]
    #[case(Id::v2("billsSSH_utf∞", None::<String>))]
    #[case(Id::v2("billsSSH_3.6.3q3", Some("with-comment")))]
    #[case(Id::v2("billsSSH_3.6.3q3", Some("utf∞-comment")))]
    #[case(Id::v2("billsSSH_3.6.3q3", Some("")))] fn it_reparses_consistently(#[case] id: Id) {
        assert_eq!(id, id.to_string().parse().unwrap());
    }
    #[rstest]
    #[case(b"")]
    #[case(&[255])]
    #[case(&[255, 255])]
    #[case(b"SSH-2.0-billsSSH_3.6.3q3\r\n")]
    #[case(b"SSH-1.99-billsSSH_3.6.3q3\n")]
    #[case(b"SSH-2.0-billsSSH_3.6.3q3 with-comment\r\n")]
    #[case(b"This is extra text\r\nIt is skipped by the parser\r\nSSH-2.0-billsSSH_3.6.3q3\r\n")]
    #[case(b"This is extra text\r\nIt is skipped by the parser\r\n")]
    #[case(b"This is extra text")]
    #[cfg(feature = "futures")]
    async fn it_reads_consistently(#[case] bytes: &[u8]) {
        assert_eq!(
            Id::from_reader(&mut std::io::BufReader::new(bytes)),
            Id::from_async_reader(&mut futures::io::BufReader::new(bytes)).await
        )
    }
    #[rstest]
    #[case(Id::v2("billsSSH_3.6.3q3", None::<String>))]
    #[case(Id::v2("billsSSH_utf∞", None::<String>))]
    #[case(Id::v2("billsSSH_3.6.3q3", Some("with-comment")))]
    #[case(Id::v2("billsSSH_3.6.3q3", Some("utf∞-comment")))]
    #[case(Id::v2("billsSSH_3.6.3q3", Some("")))] #[cfg(feature = "futures")]
    async fn it_writes_consistently(#[case] id: Id) {
        let (mut stdbuf, mut asyncbuf) = (Vec::new(), Vec::new());
        assert_eq!(
            id.to_writer(&mut stdbuf),
            id.to_async_writer(&mut asyncbuf).await
        );
        assert_eq!(stdbuf, asyncbuf);
    }
}