Toy implementation of the gemini protocol
#![feature(pattern, never_type, unwrap_infallible)]
use clap::Parser;
use media::Media;
use std::fmt::Display;

mod cli;
mod core;
mod danger;
mod media;
mod network;
mod response;
mod utils;

/// Represents a user interface
pub trait Ui {
    /// Render a page to the screen
    fn show(&mut self, content: media::Media);
    /// Display a warning
    fn warn<D: Display>(&mut self, warning: D);
    /// Display a prompt and read input from the user
    fn read(&mut self, prompt: impl Display) -> Option<String>;
    /// Display a prompt and read hidden input from the user
    fn read_secret(&mut self, prompt: impl Display) -> Option<String>;
}

/// User interface that does nothing
pub struct NopUi;
impl Ui for NopUi {
    fn show(&mut self, _content: media::Media) {}
    fn warn<D: Display>(&mut self, _warning: D) {}
    fn read(&mut self, _prompt: impl Display) -> Option<String> {
        None
    }
    fn read_secret(&mut self, _prompt: impl Display) -> Option<String> {
        None
    }
}

fn main() {
    let cli: cli::App = cli::App::parse();
    match cli.run() {
        Ok(()) => (),
        Err(e) => match e {},
    }
}

fn decode_media(mime: response::Mime, body: &str) -> Media<'_> {
    let media = match (mime.type_(), mime.subtype().as_ref()) {
        (mime::TEXT, "gemini") => Media::Gemini(body.into()),
        _ => Media::Text(body),
    };
    media
}

#[cfg(test)]
mod tests {
    use std::{
        cell::RefCell,
        fmt::Display,
        io::{Read, Write},
        rc::Rc,
    };

    use rstest::rstest;
    use url::Url;

    use crate::{media, network::Network, NopUi, Ui};

    #[rstest]
    #[case("gemini://gemini.circumlunar.space")]
    #[case("gemini://gemini.circumlunar.space/docs/gemtext.gmi")]
    fn test_connect(#[case] url: Url) {
        let mut net = crate::network::TcpNetwork::new(crate::danger::Naive);
        crate::core::run_url(&mut net, &mut NopUi, url);
    }

    #[test]
    fn test_response_body() {
        let url: Url = "gemini://gemini.circumlunar.space".parse().unwrap();
        let response = "20 \r\nsome text\r\n";
        let body = "some text\r\n";
        let mut fui = FakeUi::new();
        let mut net = FakeNet::new(FakeConn::new(response.into()));
        crate::core::run_url(&mut net, &mut fui, url.clone());
        let net_sink = Rc::try_unwrap(net.conn.0).unwrap().into_inner().sink;
        let net_sink = String::from_utf8(net_sink).unwrap();
        assert_eq!(net_sink, format!("{}\r\n", url));
        let ui_sink = fui.sink;
        assert_eq!(ui_sink.as_ref(), [FUIEvent::Show]);
    }

    /// User interface that records method calls and returns canned inputs
    #[derive(Default)]
    struct FakeUi {
        source: Vec<String>,
        sink: Vec<FUIEvent>,
    }

    #[derive(Debug, PartialEq, PartialOrd, Ord, Eq)]
    enum FUIEvent {
        //TODO figure out how to solve lifetime issues
        // and store Media in this variant
        // Or just convert it to a string? idk
        Show,
        Warn(String),
        Read(String),
        ReadSecret(String),
    }

    impl FakeUi {
        pub fn new() -> Self {
            Default::default()
        }
    }

    impl Ui for FakeUi {
        fn show(&mut self, content: media::Media) {
            self.sink.push(FUIEvent::Show)
        }
        fn warn<D: Display>(&mut self, warning: D) {
            self.sink.push(FUIEvent::Warn(warning.to_string()))
        }
        fn read(&mut self, prompt: impl Display) -> Option<String> {
            self.sink.push(FUIEvent::Read(prompt.to_string()));
            self.source.pop()
        }
        fn read_secret(&mut self, prompt: impl Display) -> Option<String> {
            self.sink.push(FUIEvent::ReadSecret(prompt.to_string()));
            self.source.pop()
        }
    }

    #[derive(Debug)]
    struct FakeNet {
        conn: SharedFakeConn,
    }
    impl FakeNet {
        fn new(conn: FakeConn) -> Self {
            Self {
                conn: SharedFakeConn(Rc::new(RefCell::new(conn))),
            }
        }
    }

    #[derive(Clone, Debug)]
    struct SharedFakeConn(Rc<RefCell<FakeConn>>);

    #[derive(Debug)]
    struct FakeConn {
        sink: Vec<u8>,
        source: Vec<u8>,
    }
    impl FakeConn {
        fn new(source: String) -> Self {
            Self {
                sink: Vec::new(),
                source: source.into_bytes(),
            }
        }
    }
    impl Read for SharedFakeConn {
        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
            self.0.borrow_mut().read(buf)
        }
    }
    impl Write for SharedFakeConn {
        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
            self.0.borrow_mut().write(buf)
        }

        fn flush(&mut self) -> std::io::Result<()> {
            self.0.borrow_mut().flush()
        }
    }
    impl Read for FakeConn {
        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
            (&*self.source).read(buf)
        }
    }
    impl Write for FakeConn {
        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
            self.sink.write(buf)
        }

        fn flush(&mut self) -> std::io::Result<()> {
            self.sink.flush()
        }
    }

    impl Network for FakeNet {
        type Error = &'static str;

        type Connection = SharedFakeConn;

        fn connect(&mut self, _: &Url) -> Result<Self::Connection, Self::Error> {
            Ok(self.conn.clone())
        }
    }

    // TODO make tests using a fake connection to a local buffer
}