use chacha20poly1305::XNonce;
use serde::{Serialize, Deserialize};
use sqlx::{
    FromRow,
    Sqlite,
    Transaction,
    types::Uuid,
};

use super::{Credential, Crypto, PersistentCredential};

use crate::errors::*;


#[derive(Debug, Clone, FromRow)]
pub struct DockerRow {
    id: Uuid,
    server_url: String,
    username: String,
    secret_enc: Vec<u8>,
    nonce: Vec<u8>,
}


#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct DockerCredential {
    #[serde(rename = "ServerURL")]
    pub server_url: String,
    pub username: String,
    pub secret: String,
}

impl PersistentCredential for DockerCredential {
    type Row = DockerRow;

    fn type_name() -> &'static str { "docker" }

    fn into_credential(self) -> Credential { Credential::Docker(self) }

    fn row_id(row: &DockerRow) -> Uuid { row.id }

    fn from_row(row: DockerRow, crypto: &Crypto) -> Result<Self, LoadCredentialsError> {
        let nonce = XNonce::clone_from_slice(&row.nonce);
        let secret_bytes = crypto.decrypt(&nonce, &row.secret_enc)?;
        let secret = String::from_utf8(secret_bytes)
            .map_err(|_| LoadCredentialsError::InvalidData)?;

        Ok(DockerCredential {
            server_url: row.server_url,
            username: row.username,
            secret
        })
    }

    async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError> {
        let (nonce, ciphertext) = crypto.encrypt(self.secret.as_bytes())?;
        let nonce_bytes = &nonce.as_slice();

        sqlx::query!(
            "INSERT OR REPLACE INTO docker_credentials (
                id,
                server_url,
                username,
                secret_enc,
                nonce
            )
            VALUES (?, ?, ?, ?, ?)",
            id, self.server_url, self.username, ciphertext, nonce_bytes,
        ).execute(&mut **txn).await?;

        Ok(())
    }
}


#[cfg(test)]
mod tests {
    use super::*;
    use crate::credentials::CredentialRecord;
    use creddy_cli::proto::DockerCredential as CliDockerCredential;
    use sqlx::SqlitePool;
    use sqlx::types::uuid::uuid;



    fn test_credential() -> DockerCredential {
        DockerCredential {
            server_url: "https://registry.jfmonty2.com".into(),
            username: "joe@jfmonty2.com".into(),
            secret: "correct horse battery staple".into(),
        }
    }

    fn test_credential_2() -> DockerCredential {
        DockerCredential {
            server_url: "https://index.docker.io/v1".into(),
            username: "test@example.com".into(),
            secret: "a very secure passphrase".into(),
        }
    }

    fn test_record() -> CredentialRecord {
        CredentialRecord {
            id: uuid!("00000000-0000-0000-0000-000000000000"),
            name: "docker_test".into(),
            is_default: false,
            credential: Credential::Docker(test_credential()),
        }
    }

    fn test_record_2() -> CredentialRecord {
        CredentialRecord {
            id: uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff"),
            name: "docker_test_2".into(),
            is_default: false,
            credential: Credential::Docker(test_credential_2()),
        }
    }


    #[sqlx::test]
    fn test_save(pool: SqlitePool) {
        let crypt = Crypto::random();
        test_record().save(&crypt, &pool).await
            .expect("Failed to save record");
    }

    #[sqlx::test(fixtures("docker_credentials"))]
    fn test_load(pool: SqlitePool) {
        let crypt = Crypto::fixed();
        let id = uuid!("00000000-0000-0000-0000-000000000000");
        let loaded = DockerCredential::load(&id, &crypt, &pool).await
            .expect("Failed to load record");

        assert_eq!(test_credential(), loaded);
    }

    #[sqlx::test(fixtures("docker_credentials"))]
    async fn test_overwrite(pool: SqlitePool) {
        let crypt = Crypto::fixed();
        let mut record = test_record_2();
        // give it the same id as test_record so that it overwrites
        let id = uuid!("00000000-0000-0000-0000-000000000000");
        record.id = id;
        record.save(&crypt, &pool).await
            .expect("Failed to overwrite original record with second record");

        let loaded = DockerCredential::load(&id, &crypt, &pool).await
            .expect("Failed to load again after overwriting");

        assert_eq!(test_credential_2(), loaded);
    }

    #[sqlx::test(fixtures("docker_credentials"))]
    async fn test_list(pool: SqlitePool) {
        let crypt = Crypto::fixed();
        let records = CredentialRecord::list(&crypt, &pool).await
            .expect("Failed to list credentials");

        assert_eq!(test_record(), records[0]);
    }


    // make sure that CLI credentials and app credentials don't drift apart
    #[test]
    fn test_cli_to_app() {
        let cli_creds = CliDockerCredential {
            server_url: "https://registry.jfmonty2.com".into(),
            username: "joe@jfmonty2.com".into(),
            secret: "correct horse battery staple".into(),
        };

        let json = serde_json::to_string(&cli_creds).unwrap();
        let computed: DockerCredential = serde_json::from_str(&json)
            .expect("Failed to deserialize Docker credentials from CLI -> main app");

        assert_eq!(test_credential(), computed);
    }

    #[test]
    fn test_app_to_cli() {
        let app_creds = test_credential();
        let json = serde_json::to_string(&app_creds).unwrap();

        let computed: CliDockerCredential = serde_json::from_str(&json)
            .expect("Failed to deserialize Docker credentials from main app -> CLI");

        let expected = CliDockerCredential {
            server_url: "https://registry.jfmonty2.com".into(),
            username: "joe@jfmonty2.com".into(),
            secret: "correct horse battery staple".into(),
        };
        assert_eq!(expected, computed);
    }
}
