use crate::error::*;
use crate::msg_types::PasswordInfo;
use crate::util;
use rusqlite::Row;
use serde_derive::*;
use std::time::{self, SystemTime};
use sync15::ServerTimestamp;
use sync_guid::Guid;
use url::Url;
#[derive(Debug, Clone, Hash, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct Login {
    #[serde(rename = "id")]
    pub guid: Guid,
    pub hostname: String,
    
    
    #[serde(rename = "formSubmitURL")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub form_submit_url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub http_realm: Option<String>,
    #[serde(default)]
    pub username: String,
    pub password: String,
    #[serde(default)]
    pub username_field: String,
    #[serde(default)]
    pub password_field: String,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_timestamp")]
    pub time_created: i64,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_timestamp")]
    pub time_password_changed: i64,
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_timestamp")]
    pub time_last_used: i64,
    #[serde(default)]
    pub times_used: i64,
}
fn deserialize_timestamp<'de, D>(deserializer: D) -> std::result::Result<i64, D::Error>
where
    D: serde::de::Deserializer<'de>,
{
    use serde::de::Deserialize;
    
    
    
    
    Ok(i64::deserialize(deserializer).unwrap_or_default().max(0))
}
fn string_or_default(row: &Row<'_>, col: &str) -> Result<String> {
    Ok(row.get::<_, Option<String>>(col)?.unwrap_or_default())
}
impl Login {
    #[inline]
    pub fn guid(&self) -> &Guid {
        &self.guid
    }
    #[inline]
    pub fn guid_str(&self) -> &str {
        self.guid.as_str()
    }
    
    
    pub fn check_valid(&self) -> Result<()> {
        self.validate_and_fixup(false)?;
        Ok(())
    }
    
    
    
    
    pub fn fixup(self) -> Result<Self> {
        match self.maybe_fixup()? {
            None => Ok(self),
            Some(login) => Ok(login),
        }
    }
    
    
    
    pub fn maybe_fixup(&self) -> Result<Option<Self>> {
        self.validate_and_fixup(true)
    }
    
    
    fn validate_and_fixup_origin(origin: &str) -> Result<Option<String>> {
        
        match Url::parse(&origin) {
            Ok(mut u) => {
                
                if u.path() != "/"
                    || u.fragment().is_some()
                    || u.query().is_some()
                    || u.username() != "/"
                    || u.password().is_some()
                {
                    
                    
                    
                    
                    if u.scheme() == "file" {
                        return Ok(if origin == "file://" {
                            None
                        } else {
                            Some("file://".into())
                        });
                    }
                    u.set_path("");
                    u.set_fragment(None);
                    u.set_query(None);
                    let _ = u.set_username("");
                    let _ = u.set_password(None);
                    let mut href = u.into_string();
                    
                    if href.ends_with('/') {
                        href.pop().expect("url must have a length");
                    }
                    if origin != href {
                        
                        return Ok(Some(href));
                    }
                }
                Ok(None)
            }
            Err(_) => {
                
                throw!(InvalidLogin::IllegalFieldValue {
                    field_info: "Origin is Malformed".into()
                });
            }
        }
    }
    
    fn validate_and_fixup(&self, fixup: bool) -> Result<Option<Self>> {
        
        let mut maybe_fixed = None;
        
        macro_rules! get_fixed_or_throw {
            ($err:expr) => {
                
                
                {
                    if !fixup {
                        throw!($err)
                    }
                    log::warn!("Fixing login record {}: {:?}", self.guid, $err);
                    let fixed: Result<&mut Login> =
                        Ok(maybe_fixed.get_or_insert_with(|| self.clone()));
                    fixed
                }
            };
        };
        if self.hostname.is_empty() {
            throw!(InvalidLogin::EmptyOrigin);
        }
        if self.password.is_empty() {
            throw!(InvalidLogin::EmptyPassword);
        }
        if self.form_submit_url.is_some() && self.http_realm.is_some() {
            get_fixed_or_throw!(InvalidLogin::BothTargets)?.http_realm = None;
        }
        if self.form_submit_url.is_none() && self.http_realm.is_none() {
            throw!(InvalidLogin::NoTarget);
        }
        let form_submit_url = self.form_submit_url.clone().unwrap_or_default();
        let http_realm = maybe_fixed
            .as_ref()
            .unwrap_or(self)
            .http_realm
            .clone()
            .unwrap_or_default();
        let field_data = [
            ("formSubmitUrl", &form_submit_url),
            ("httpRealm", &http_realm),
            ("hostname", &self.hostname),
            ("usernameField", &self.username_field),
            ("passwordField", &self.password_field),
            ("username", &self.username),
            ("password", &self.password),
        ];
        for (field_name, field_value) in &field_data {
            
            if field_value.contains('\0') {
                throw!(InvalidLogin::IllegalFieldValue {
                    field_info: format!("`{}` contains Nul", field_name)
                });
            }
            
            
            if field_name != &"username"
                && field_name != &"password"
                && (field_value.contains('\n') || field_value.contains('\r'))
            {
                throw!(InvalidLogin::IllegalFieldValue {
                    field_info: format!("`{}` contains newline", field_name)
                });
            }
        }
        
        if self.username_field == "." {
            throw!(InvalidLogin::IllegalFieldValue {
                field_info: "`usernameField` is a period".into()
            });
        }
        
        if let Some(fixed) = Login::validate_and_fixup_origin(&self.hostname)? {
            get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
                field_info: "Origin is not normalized".into()
            })?
            .hostname = fixed;
        }
        match &maybe_fixed.as_ref().unwrap_or(self).form_submit_url {
            None => {
                if !self.username_field.is_empty() {
                    get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
                        field_info: "usernameField must be empty when formSubmitURL is null".into()
                    })?
                    .username_field
                    .clear();
                }
                if !self.password_field.is_empty() {
                    get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
                        field_info: "passwordField must be empty when formSubmitURL is null".into()
                    })?
                    .password_field
                    .clear();
                }
            }
            Some(href) => {
                
                if href == "." {
                    
                    
                    if fixup {
                        maybe_fixed
                            .get_or_insert_with(|| self.clone())
                            .form_submit_url = Some("".into());
                    }
                } else if href != "" && href != "javascript:" {
                    if let Some(fixed) = Login::validate_and_fixup_origin(&href)? {
                        get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
                            field_info: "formActionOrigin is not normalized".into()
                        })?
                        .form_submit_url = Some(fixed);
                    }
                }
            }
        }
        Ok(maybe_fixed)
    }
    pub(crate) fn from_row(row: &Row<'_>) -> Result<Login> {
        let login = Login {
            guid: row.get("guid")?,
            password: row.get("password")?,
            username: string_or_default(row, "username")?,
            hostname: row.get("hostname")?,
            http_realm: row.get("httpRealm")?,
            form_submit_url: row.get("formSubmitURL")?,
            username_field: string_or_default(row, "usernameField")?,
            password_field: string_or_default(row, "passwordField")?,
            time_created: row.get("timeCreated")?,
            
            time_last_used: row
                .get::<_, Option<i64>>("timeLastUsed")?
                .unwrap_or_default(),
            time_password_changed: row.get("timePasswordChanged")?,
            times_used: row.get("timesUsed")?,
        };
        
        
        Ok(login.maybe_fixup().unwrap_or(None).unwrap_or(login))
    }
}
impl From<Login> for PasswordInfo {
    fn from(login: Login) -> Self {
        Self {
            id: login.guid.into_string(),
            hostname: login.hostname,
            password: login.password,
            username: login.username,
            http_realm: login.http_realm,
            form_submit_url: login.form_submit_url,
            username_field: login.username_field,
            password_field: login.password_field,
            times_used: login.times_used,
            time_created: login.time_created,
            time_last_used: login.time_last_used,
            time_password_changed: login.time_password_changed,
        }
    }
}
impl From<PasswordInfo> for Login {
    fn from(info: PasswordInfo) -> Self {
        Self {
            guid: Guid::from_string(info.id),
            hostname: info.hostname,
            password: info.password,
            username: info.username,
            http_realm: info.http_realm,
            form_submit_url: info.form_submit_url,
            username_field: info.username_field,
            password_field: info.password_field,
            times_used: info.times_used,
            time_created: info.time_created,
            time_last_used: info.time_last_used,
            time_password_changed: info.time_password_changed,
        }
    }
}
#[derive(Clone, Debug)]
pub(crate) struct MirrorLogin {
    pub login: Login,
    pub is_overridden: bool,
    pub server_modified: ServerTimestamp,
}
impl MirrorLogin {
    #[inline]
    pub fn guid_str(&self) -> &str {
        self.login.guid_str()
    }
    pub(crate) fn from_row(row: &Row<'_>) -> Result<MirrorLogin> {
        Ok(MirrorLogin {
            login: Login::from_row(row)?,
            is_overridden: row.get("is_overridden")?,
            server_modified: ServerTimestamp(row.get::<_, i64>("server_modified")?),
        })
    }
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[repr(u8)]
pub(crate) enum SyncStatus {
    Synced = 0,
    Changed = 1,
    New = 2,
}
impl SyncStatus {
    #[inline]
    pub fn from_u8(v: u8) -> Result<Self> {
        match v {
            0 => Ok(SyncStatus::Synced),
            1 => Ok(SyncStatus::Changed),
            2 => Ok(SyncStatus::New),
            v => throw!(ErrorKind::BadSyncStatus(v)),
        }
    }
}
#[derive(Clone, Debug)]
pub(crate) struct LocalLogin {
    pub login: Login,
    pub sync_status: SyncStatus,
    pub is_deleted: bool,
    pub local_modified: SystemTime,
}
impl LocalLogin {
    #[inline]
    pub fn guid_str(&self) -> &str {
        self.login.guid_str()
    }
    pub(crate) fn from_row(row: &Row<'_>) -> Result<LocalLogin> {
        Ok(LocalLogin {
            login: Login::from_row(row)?,
            sync_status: SyncStatus::from_u8(row.get("sync_status")?)?,
            is_deleted: row.get("is_deleted")?,
            local_modified: util::system_time_millis_from_row(row, "local_modified")?,
        })
    }
}
macro_rules! impl_login {
    ($ty:ty { $($fields:tt)* }) => {
        impl AsRef<Login> for $ty {
            #[inline]
            fn as_ref(&self) -> &Login {
                &self.login
            }
        }
        impl AsMut<Login> for $ty {
            #[inline]
            fn as_mut(&mut self) -> &mut Login {
                &mut self.login
            }
        }
        impl From<$ty> for Login {
            #[inline]
            fn from(l: $ty) -> Self {
                l.login
            }
        }
        impl From<Login> for $ty {
            #[inline]
            fn from(login: Login) -> Self {
                Self { login, $($fields)* }
            }
        }
    };
}
impl_login!(LocalLogin {
    sync_status: SyncStatus::New,
    is_deleted: false,
    local_modified: time::UNIX_EPOCH
});
impl_login!(MirrorLogin {
    is_overridden: false,
    server_modified: ServerTimestamp(0)
});
pub(crate) struct SyncLoginData {
    pub guid: Guid,
    pub local: Option<LocalLogin>,
    pub mirror: Option<MirrorLogin>,
    
    pub inbound: (Option<Login>, ServerTimestamp),
}
impl SyncLoginData {
    #[inline]
    pub fn guid_str(&self) -> &str {
        &self.guid.as_str()
    }
    #[inline]
    pub fn guid(&self) -> &Guid {
        &self.guid
    }
    
    
    pub fn from_payload(
        payload: sync15::Payload,
        ts: ServerTimestamp,
    ) -> std::result::Result<Self, serde_json::Error> {
        let guid = payload.id.clone();
        let login: Option<Login> = if payload.is_tombstone() {
            None
        } else {
            let record: Login = payload.into_record()?;
            
            
            record.maybe_fixup().unwrap_or(None).or(Some(record))
        };
        Ok(Self {
            guid,
            local: None,
            mirror: None,
            inbound: (login, ts),
        })
    }
}
macro_rules! impl_login_setter {
    ($setter_name:ident, $field:ident, $Login:ty) => {
        impl SyncLoginData {
            pub(crate) fn $setter_name(&mut self, record: $Login) -> Result<()> {
                
                if self.$field.is_some() {
                    
                    
                    panic!(
                        "SyncLoginData::{} called on object that already has {} data",
                        stringify!($setter_name),
                        stringify!($field)
                    );
                }
                if self.guid_str() != record.guid_str() {
                    
                    panic!(
                        "Wrong guid on login in {}: {:?} != {:?}",
                        stringify!($setter_name),
                        self.guid_str(),
                        record.guid_str()
                    );
                }
                self.$field = Some(record);
                Ok(())
            }
        }
    };
}
impl_login_setter!(set_local, local, LocalLogin);
impl_login_setter!(set_mirror, mirror, MirrorLogin);
#[derive(Debug, Default, Clone)]
pub(crate) struct LoginDelta {
    
    pub hostname: Option<String>,
    pub password: Option<String>,
    pub username: Option<String>,
    pub http_realm: Option<String>,
    pub form_submit_url: Option<String>,
    pub time_created: Option<i64>,
    pub time_last_used: Option<i64>,
    pub time_password_changed: Option<i64>,
    
    pub password_field: Option<String>,
    pub username_field: Option<String>,
    
    pub times_used: i64,
}
macro_rules! merge_field {
    ($merged:ident, $b:ident, $prefer_b:expr, $field:ident) => {
        if let Some($field) = $b.$field.take() {
            if $merged.$field.is_some() {
                log::warn!("Collision merging login field {}", stringify!($field));
                if $prefer_b {
                    $merged.$field = Some($field);
                }
            } else {
                $merged.$field = Some($field);
            }
        }
    };
}
impl LoginDelta {
    #[allow(clippy::cognitive_complexity)] 
    pub fn merge(self, mut b: LoginDelta, b_is_newer: bool) -> LoginDelta {
        let mut merged = self;
        merge_field!(merged, b, b_is_newer, hostname);
        merge_field!(merged, b, b_is_newer, password);
        merge_field!(merged, b, b_is_newer, username);
        merge_field!(merged, b, b_is_newer, http_realm);
        merge_field!(merged, b, b_is_newer, form_submit_url);
        merge_field!(merged, b, b_is_newer, time_created);
        merge_field!(merged, b, b_is_newer, time_last_used);
        merge_field!(merged, b, b_is_newer, time_password_changed);
        merge_field!(merged, b, b_is_newer, password_field);
        merge_field!(merged, b, b_is_newer, username_field);
        
        merged.times_used += b.times_used;
        merged
    }
}
macro_rules! apply_field {
    ($login:ident, $delta:ident, $field:ident) => {
        if let Some($field) = $delta.$field.take() {
            $login.$field = $field.into();
        }
    };
}
impl Login {
    pub(crate) fn apply_delta(&mut self, mut delta: LoginDelta) {
        apply_field!(self, delta, hostname);
        apply_field!(self, delta, password);
        apply_field!(self, delta, username);
        apply_field!(self, delta, time_created);
        apply_field!(self, delta, time_last_used);
        apply_field!(self, delta, time_password_changed);
        apply_field!(self, delta, password_field);
        apply_field!(self, delta, username_field);
        
        if let Some(realm) = delta.http_realm.take() {
            self.http_realm = if realm.is_empty() { None } else { Some(realm) };
        }
        if let Some(url) = delta.form_submit_url.take() {
            self.form_submit_url = if url.is_empty() { None } else { Some(url) };
        }
        self.times_used += delta.times_used;
    }
    pub(crate) fn delta(&self, older: &Login) -> LoginDelta {
        let mut delta = LoginDelta::default();
        if self.form_submit_url != older.form_submit_url {
            delta.form_submit_url = Some(self.form_submit_url.clone().unwrap_or_default());
        }
        if self.http_realm != older.http_realm {
            delta.http_realm = Some(self.http_realm.clone().unwrap_or_default());
        }
        if self.hostname != older.hostname {
            delta.hostname = Some(self.hostname.clone());
        }
        if self.username != older.username {
            delta.username = Some(self.username.clone());
        }
        if self.password != older.password {
            delta.password = Some(self.password.clone());
        }
        if self.password_field != older.password_field {
            delta.password_field = Some(self.password_field.clone());
        }
        if self.username_field != older.username_field {
            delta.username_field = Some(self.username_field.clone());
        }
        
        
        
        
        
        
        
        
        
        if self.time_created > 0 && self.time_created != older.time_created {
            delta.time_created = Some(self.time_created);
        }
        if self.time_last_used > 0 && self.time_last_used != older.time_last_used {
            delta.time_last_used = Some(self.time_last_used);
        }
        if self.time_password_changed > 0
            && self.time_password_changed != older.time_password_changed
        {
            delta.time_password_changed = Some(self.time_password_changed);
        }
        if self.times_used > 0 && self.times_used != older.times_used {
            delta.times_used = self.times_used - older.times_used;
        }
        delta
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_invalid_payload_timestamps() {
        #[allow(clippy::unreadable_literal)]
        let bad_timestamp = 18446732429235952000u64;
        let bad_payload: sync15::Payload = serde_json::from_value(serde_json::json!({
            "id": "123412341234",
            "formSubmitURL": "https://www.example.com/submit",
            "hostname": "https://www.example.com",
            "username": "test",
            "password": "test",
            "timeCreated": bad_timestamp,
            "timeLastUsed": "some other garbage",
            "timePasswordChanged": -30, 
        }))
        .unwrap();
        let login = SyncLoginData::from_payload(bad_payload, ServerTimestamp::default())
            .unwrap()
            .inbound
            .0
            .unwrap();
        assert_eq!(login.time_created, 0);
        assert_eq!(login.time_last_used, 0);
        assert_eq!(login.time_password_changed, 0);
        let now64 = util::system_time_ms_i64(std::time::SystemTime::now());
        let good_payload: sync15::Payload = serde_json::from_value(serde_json::json!({
            "id": "123412341234",
            "formSubmitURL": "https://www.example.com/submit",
            "hostname": "https://www.example.com",
            "username": "test",
            "password": "test",
            "timeCreated": now64 - 100,
            "timeLastUsed": now64 - 50,
            "timePasswordChanged": now64 - 25,
        }))
        .unwrap();
        let login = SyncLoginData::from_payload(good_payload, ServerTimestamp::default())
            .unwrap()
            .inbound
            .0
            .unwrap();
        assert_eq!(login.time_created, now64 - 100);
        assert_eq!(login.time_last_used, now64 - 50);
        assert_eq!(login.time_password_changed, now64 - 25);
    }
    #[test]
    fn test_url_fixups() -> Result<()> {
        
        for input in &[
            
            "https://site.com",
            "http://site.com:1234",
            "ftp://ftp.site.com",
            "moz-proxy://127.0.0.1:8888",
            "chrome://MyLegacyExtension",
            "file://",
            "https://[::1]",
        ] {
            assert_eq!(Login::validate_and_fixup_origin(input)?, None);
        }
        
        for (input, output) in &[
            ("https://site.com/", "https://site.com"),
            ("http://site.com:1234/", "http://site.com:1234"),
            ("http://example.com/foo?query=wtf#bar", "http://example.com"),
            ("http://example.com/foo#bar", "http://example.com"),
            (
                "http://username:password@example.com/",
                "http://example.com",
            ),
            ("http://😍.com/", "http://xn--r28h.com"),
            ("https://[0:0:0:0:0:0:0:1]", "https://[::1]"),
            
            
            ("file:///", "file://"),
            ("file://foo/bar", "file://"),
            ("file://foo/bar/", "file://"),
            ("moz-proxy://127.0.0.1:8888/", "moz-proxy://127.0.0.1:8888"),
            (
                "moz-proxy://127.0.0.1:8888/foo",
                "moz-proxy://127.0.0.1:8888",
            ),
            ("chrome://MyLegacyExtension/", "chrome://MyLegacyExtension"),
            (
                "chrome://MyLegacyExtension/foo",
                "chrome://MyLegacyExtension",
            ),
        ] {
            assert_eq!(
                Login::validate_and_fixup_origin(input)?,
                Some((*output).into())
            );
        }
        Ok(())
    }
    #[test]
    fn test_check_valid() {
        #[derive(Debug, Clone)]
        struct TestCase {
            login: Login,
            should_err: bool,
            expected_err: &'static str,
        }
        let valid_login = Login {
            hostname: "https://www.example.com".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_empty_hostname = Login {
            hostname: "".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_empty_password = Login {
            hostname: "https://www.example.com".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "".into(),
            ..Login::default()
        };
        let login_with_form_submit_and_http_realm = Login {
            hostname: "https://www.example.com".into(),
            http_realm: Some("https://www.example.com".into()),
            form_submit_url: Some("https://www.example.com".into()),
            password: "test".into(),
            ..Login::default()
        };
        let login_without_form_submit_or_http_realm = Login {
            hostname: "https://www.example.com".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_null_http_realm = Login {
            hostname: "https://www.example.com".into(),
            http_realm: Some("https://www.example.\0com".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_null_username = Login {
            hostname: "https://www.example.com".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "\0".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_null_password = Login {
            hostname: "https://www.example.com".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "username".into(),
            password: "test\0".into(),
            ..Login::default()
        };
        let login_with_newline_hostname = Login {
            hostname: "\rhttps://www.example.com".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_newline_username_field = Login {
            hostname: "https://www.example.com".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "test".into(),
            username_field: "\n".into(),
            ..Login::default()
        };
        let login_with_newline_realm = Login {
            hostname: "https://www.example.com".into(),
            http_realm: Some("foo\nbar".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_newline_password = Login {
            hostname: "https://www.example.com".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "test\n".into(),
            ..Login::default()
        };
        let login_with_period_username_field = Login {
            hostname: "https://www.example.com".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "test".into(),
            username_field: ".".into(),
            ..Login::default()
        };
        let login_with_period_form_submit_url = Login {
            form_submit_url: Some(".".into()),
            hostname: "https://www.example.com".into(),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_javascript_form_submit_url = Login {
            form_submit_url: Some("javascript:".into()),
            hostname: "https://www.example.com".into(),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_malformed_origin_parens = Login {
            hostname: " (".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_host_unicode = Login {
            hostname: "http://💖.com".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_hostname_trailing_slash = Login {
            hostname: "https://www.example.com/".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_hostname_expanded_ipv6 = Login {
            hostname: "https://[0:0:0:0:0:0:1:1]".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_unknown_protocol = Login {
            hostname: "moz-proxy://127.0.0.1:8888".into(),
            http_realm: Some("https://www.example.com".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let test_cases = [
            TestCase {
                login: valid_login,
                should_err: false,
                expected_err: "",
            },
            TestCase {
                login: login_with_empty_hostname,
                should_err: true,
                expected_err: "Invalid login: Origin is empty",
            },
            TestCase {
                login: login_with_empty_password,
                should_err: true,
                expected_err: "Invalid login: Password is empty",
            },
            TestCase {
                login: login_with_form_submit_and_http_realm,
                should_err: true,
                expected_err: "Invalid login: Both `formSubmitUrl` and `httpRealm` are present",
            },
            TestCase {
                login: login_without_form_submit_or_http_realm,
                should_err: true,
                expected_err: "Invalid login: Neither `formSubmitUrl` or `httpRealm` are present",
            },
            TestCase {
                login: login_with_null_http_realm,
                should_err: true,
                expected_err: "Invalid login: Login has illegal field: `httpRealm` contains Nul",
            },
            TestCase {
                login: login_with_null_username,
                should_err: true,
                expected_err: "Invalid login: Login has illegal field: `username` contains Nul",
            },
            TestCase {
                login: login_with_null_password,
                should_err: true,
                expected_err: "Invalid login: Login has illegal field: `password` contains Nul",
            },
            TestCase {
                login: login_with_newline_hostname,
                should_err: true,
                expected_err: "Invalid login: Login has illegal field: `hostname` contains newline",
            },
            TestCase {
                login: login_with_newline_realm,
                should_err: true,
                expected_err:
                    "Invalid login: Login has illegal field: `httpRealm` contains newline",
            },
            TestCase {
                login: login_with_newline_username_field,
                should_err: true,
                expected_err:
                    "Invalid login: Login has illegal field: `usernameField` contains newline",
            },
            TestCase {
                login: login_with_newline_password,
                should_err: false,
                expected_err: "",
            },
            TestCase {
                login: login_with_period_username_field,
                should_err: true,
                expected_err: "Invalid login: Login has illegal field: `usernameField` is a period",
            },
            TestCase {
                login: login_with_period_form_submit_url,
                should_err: false,
                expected_err: "",
            },
            TestCase {
                login: login_with_javascript_form_submit_url,
                should_err: false,
                expected_err: "",
            },
            TestCase {
                login: login_with_malformed_origin_parens,
                should_err: true,
                expected_err: "Invalid login: Login has illegal field: Origin is Malformed",
            },
            TestCase {
                login: login_with_host_unicode,
                should_err: true,
                expected_err: "Invalid login: Login has illegal field: Origin is not normalized",
            },
            TestCase {
                login: login_with_hostname_trailing_slash,
                should_err: true,
                expected_err: "Invalid login: Login has illegal field: Origin is not normalized",
            },
            TestCase {
                login: login_with_hostname_expanded_ipv6,
                should_err: true,
                expected_err: "Invalid login: Login has illegal field: Origin is not normalized",
            },
            TestCase {
                login: login_with_unknown_protocol,
                should_err: false,
                expected_err: "",
            },
        ];
        for tc in &test_cases {
            let actual = tc.login.check_valid();
            if tc.should_err {
                assert!(actual.is_err(), "{:#?}", tc);
                assert_eq!(
                    tc.expected_err,
                    actual.unwrap_err().to_string(),
                    "{:#?}",
                    tc,
                );
            } else {
                assert!(actual.is_ok(), "{:#?}", tc);
                assert!(
                    tc.login.clone().fixup().is_ok(),
                    "Fixup failed after check_valid passed: {:#?}",
                    &tc,
                );
            }
        }
    }
    #[test]
    fn test_fixup() {
        #[derive(Debug, Default)]
        struct TestCase {
            login: Login,
            fixedup_host: Option<&'static str>,
            fixedup_form_submit_url: Option<String>,
        }
        
        let login_with_full_url = Login {
            hostname: "http://example.com/foo?query=wtf#bar".into(),
            form_submit_url: Some("http://example.com/foo?query=wtf#bar".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_host_unicode = Login {
            hostname: "http://😍.com".into(),
            form_submit_url: Some("http://😍.com".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_period_fsu = Login {
            hostname: "https://example.com".into(),
            form_submit_url: Some(".".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_empty_fsu = Login {
            hostname: "https://example.com".into(),
            form_submit_url: Some("".into()),
            username: "test".into(),
            password: "test".into(),
            ..Login::default()
        };
        let login_with_form_submit_and_http_realm = Login {
            hostname: "https://www.example.com".into(),
            form_submit_url: Some("https://www.example.com".into()),
            
            
            
            
            http_realm: Some("\n".into()),
            password: "test".into(),
            ..Login::default()
        };
        let test_cases = [
            TestCase {
                login: login_with_full_url,
                fixedup_host: "http://example.com".into(),
                fixedup_form_submit_url: Some("http://example.com".into()),
            },
            TestCase {
                login: login_with_host_unicode,
                fixedup_host: "http://xn--r28h.com".into(),
                fixedup_form_submit_url: Some("http://xn--r28h.com".into()),
            },
            TestCase {
                login: login_with_period_fsu,
                fixedup_form_submit_url: Some("".into()),
                ..TestCase::default()
            },
            TestCase {
                login: login_with_form_submit_and_http_realm,
                fixedup_form_submit_url: Some("https://www.example.com".into()),
                ..TestCase::default()
            },
            TestCase {
                login: login_with_empty_fsu,
                
                fixedup_form_submit_url: Some("".into()),
                ..TestCase::default()
            },
        ];
        for tc in &test_cases {
            let login = tc.login.clone().fixup().expect("should work");
            if let Some(expected) = tc.fixedup_host {
                assert_eq!(login.hostname, expected, "hostname not fixed in {:#?}", tc);
            }
            assert_eq!(
                login.form_submit_url, tc.fixedup_form_submit_url,
                "form_submit_url not fixed in {:#?}",
                tc,
            );
            login.check_valid().unwrap_or_else(|e| {
                panic!("Fixup produces invalid record: {:#?}", (e, &tc, &login));
            });
            assert_eq!(
                login.clone().fixup().unwrap(),
                login,
                "fixup did not reach fixed point for testcase: {:#?}",
                tc,
            );
        }
    }
    #[test]
    fn test_username_field_requires_a_form_target() {
        let bad_payload: sync15::Payload = serde_json::from_value(serde_json::json!({
            "id": "123412341234",
            "httpRealm": "test",
            "hostname": "https://www.example.com",
            "username": "test",
            "password": "test",
            "usernameField": "invalid"
        }))
        .unwrap();
        let login: Login = bad_payload.clone().into_record().unwrap();
        assert_eq!(login.username_field, "invalid");
        assert!(login.check_valid().is_err());
        assert_eq!(login.fixup().unwrap().username_field, "");
        
        let login = SyncLoginData::from_payload(bad_payload, ServerTimestamp::default())
            .unwrap()
            .inbound
            .0
            .unwrap();
        assert_eq!(login.username_field, "");
    }
    #[test]
    fn test_password_field_requires_a_form_target() {
        let bad_payload: sync15::Payload = serde_json::from_value(serde_json::json!({
            "id": "123412341234",
            "httpRealm": "test",
            "hostname": "https://www.example.com",
            "username": "test",
            "password": "test",
            "passwordField": "invalid"
        }))
        .unwrap();
        let login: Login = bad_payload.into_record().unwrap();
        assert_eq!(login.password_field, "invalid");
        assert!(login.check_valid().is_err());
        assert_eq!(login.fixup().unwrap().password_field, "");
    }
}