use url_parse::core::Parser;

use crate::netrc::netrc;

#[derive(Debug)]
pub struct ParsedAddress {
    pub server: String,
    pub username: String,
    pub password: String,
    pub path_segments: Vec<String>,
    pub file: String,
}

impl PartialEq for ParsedAddress {
    fn eq(&self, other: &Self) -> bool {
        let result = self.server == other.server
            && self.username == other.username
            && self.password == other.password
            && self.file == other.file;

        let mut paths_equal = true;
        for it in self.path_segments.iter().zip(self.path_segments.iter()) {
            let (left, right) = it;
            paths_equal = paths_equal && (left == right);
        }

        result && paths_equal
    }
}

impl ParsedAddress {
    pub fn parse_address(address: &str, silent: bool) -> ParsedAddress {
        let netrc = netrc(silent);
        let url = Parser::new(None).parse(address).unwrap();
        let server = format!(
            "{}:{}",
            url.host_str()
                .ok_or_else(|| panic!("failed to parse hostname from url: {url}"))
                .unwrap(),
            url.port_or_known_default()
                .ok_or_else(|| panic!("failed to parse port from url: {url}"))
                .unwrap(),
        );

        let url_username = url.username();
        let username = if url_username.is_none() {
            "anonymous".to_string()
        } else {
            url.username().unwrap()
        };

        let password = url.password().unwrap_or_else(|| "anonymous".to_string());
        if !silent && username != "anonymous" && password != "anonymous" {
            println!("🔑 Parsed credentials from URL.");
        }

        let (username, password) = ParsedAddress::mixin_netrc(&netrc, &server, username, password);

        let mut path_segments: Vec<String> = url
            .path_segments()
            .ok_or_else(|| panic!("failed to get url path segments: {url}"))
            .unwrap();

        let file = path_segments
            .pop()
            .ok_or_else(|| panic!("got empty path segments from url: {url}"))
            .unwrap();

        ParsedAddress {
            server,
            username,
            password,
            path_segments,
            file,
        }
    }

    fn mixin_netrc(
        netrc: &Option<netrc::Netrc>,
        server: &str,
        username: String,
        password: String,
    ) -> (String, String) {
        let mut user = username.clone();
        let mut pass = password.clone();
        if !netrc.is_none() && username == "anonymous" && password == "anonymous" {
            for host in netrc.as_ref().unwrap().hosts.iter().enumerate() {
                let (_i, (netrc_name, machine)) = host;

                let mut name = netrc_name.to_string();
                if let Some(port) = machine.port {
                    name = name + ":" + &port.to_string()[..];
                }
                if server == name {
                    user.clone_from(&machine.login);
                    pass = machine.password.clone().unwrap();
                    break;
                }
            }
        }
        (user, pass)
    }
}

#[tokio::test]
async fn parseaddress_operator_equals_works_when_typical() {
    let left = ParsedAddress {
        server: "do.main".to_string(),
        username: "user".to_string(),
        password: "pass".to_string(),
        path_segments: vec!["my".to_string(), "path".to_string()],
        file: "pass".to_string(),
    };
    let right = ParsedAddress {
        server: "do.main".to_string(),
        username: "user".to_string(),
        password: "pass".to_string(),
        path_segments: vec!["my".to_string(), "path".to_string()],
        file: "pass".to_string(),
    };

    assert!(left == right);
}

#[tokio::test]
async fn parseaddress_operator_equals_fails_when_not_equal() {
    let left = ParsedAddress {
        server: "do.main".to_string(),
        username: "user".to_string(),
        password: "pass".to_string(),
        path_segments: vec!["my".to_string(), "path".to_string()],
        file: "pass".to_string(),
    };
    let right = ParsedAddress {
        server: "do".to_string(),
        username: "user".to_string(),
        password: "pass".to_string(),
        path_segments: vec!["my".to_string(), "path".to_string()],
        file: "pass".to_string(),
    };

    assert!(left != right);
}

#[tokio::test]
async fn parse_works() {
    let expected = ParsedAddress {
        server: "do.main:21".to_string(),
        username: "user".to_string(),
        password: "pass".to_string(),
        path_segments: vec!["index".to_string()],
        file: "file".to_string(),
    };

    let actual = ParsedAddress::parse_address("ftp://user:pass@do.main:21/index/file", true);

    assert_eq!(actual, expected);
}

#[tokio::test]
async fn mixin_works() {
    let expected_username = "test";
    let expected_password = "p@ssw0rd";
    let input = "machine example.com login test password p@ssw0rd";
    let input = std::io::BufReader::new(input.as_bytes());
    let netrc = netrc::Netrc::parse(input).unwrap();
    let username_decoded_from_url = "anonymous".to_string();
    let password_decoded_from_url = "anonymous".to_string();

    let (actual_username, actual_password) = ParsedAddress::mixin_netrc(
        &Some(netrc),
        "example.com",
        username_decoded_from_url,
        password_decoded_from_url,
    );

    assert_eq!(actual_username, expected_username);
    assert_eq!(actual_password, expected_password);
}

#[tokio::test]
async fn mixin_works_with_port() {
    let expected_username = "test";
    let expected_password = "p@ssw0rd";
    let input = "machine example.com login test password p@ssw0rd port 443";
    let input = std::io::BufReader::new(input.as_bytes());
    let netrc = netrc::Netrc::parse(input).unwrap();
    let username_decoded_from_url = "anonymous".to_string();
    let password_decoded_from_url = "anonymous".to_string();

    let (actual_username, actual_password) = ParsedAddress::mixin_netrc(
        &Some(netrc),
        "example.com:443",
        username_decoded_from_url,
        password_decoded_from_url,
    );

    assert_eq!(actual_username, expected_username);
    assert_eq!(actual_password, expected_password);
}

#[tokio::test]
async fn parse_works_with_netrc_mixin() {
    let expected = ParsedAddress {
        server: "do.main:21".to_string(),
        username: "test".to_string(),
        password: "p@ssw0rd".to_string(),
        path_segments: vec!["index".to_string()],
        file: "file".to_string(),
    };
    let data = "machine do.main login test password p@ssw0rd port 21";

    std::fs::write(".netrc.test", data).expect("Unable to write file");
    let actual = ParsedAddress::parse_address("ftp://do.main/index/file", true);

    assert_eq!(actual, expected);
    std::fs::remove_file(".netrc.test").unwrap();
}

#[tokio::test]
async fn parse_works_when_ssh_user() {
    let expected = ParsedAddress {
        server: "localhost:2223".to_string(),
        username: "user".to_string(),
        password: "anonymous".to_string(),
        path_segments: vec!["".to_string()],
        file: "file".to_string(),
    };

    let actual = ParsedAddress::parse_address("ssh://user@localhost:2223/file", true);

    assert_eq!(actual, expected);
}

#[tokio::test]
async fn parse_works_when_not_silent() {
    let expected = ParsedAddress {
        server: "localhost:2223".to_string(),
        username: "user".to_string(),
        password: "pass".to_string(),
        path_segments: vec!["".to_string()],
        file: "file".to_string(),
    };

    let actual = ParsedAddress::parse_address("ssh://user:pass@localhost:2223/file", false);

    assert_eq!(actual, expected);
}
