mod api;
mod assets;
mod debug;
mod errors;
mod index;
pub mod maintenance;
pub mod manage;
mod pks;
pub mod vks;
mod wkd;

use rocket::{Route, routes};

pub fn routes() -> Vec<Route> {
    routes![
        // infra
        index::root,
        assets::files,
        errors::errors,
        // VKSv1
        api::rest::vks::vks_v1_by_email,
        api::rest::vks::vks_v1_by_fingerprint,
        api::rest::vks::vks_v1_by_keyid,
        api::rest::vks::upload_json,
        api::rest::vks::upload_fallback,
        api::rest::vks::request_verify_json,
        api::rest::vks::request_verify_fallback,
        // User interaction.
        vks::search,
        vks::upload,
        vks::upload_post_form,
        vks::upload_post_form_data,
        vks::request_verify_form,
        vks::request_verify_form_data,
        vks::verify_confirm,
        vks::verify_confirm_form,
        vks::quick_upload,
        vks::quick_upload_proceed,
        // Debug
        debug::debug_info,
        // HKP
        pks::pks_lookup,
        pks::pks_add_form,
        pks::pks_add_form_data,
        pks::pks_internal_index,
        // WKD
        wkd::wkd_policy,
        wkd::wkd_query,
        // Manage
        manage::vks_manage,
        manage::vks_manage_key,
        manage::vks_manage_post,
        manage::vks_manage_unpublish,
        // Maintenance error page
        maintenance::maintenance_error_web,
        maintenance::maintenance_error_json,
        maintenance::maintenance_error_plain,
    ]
}

#[cfg(test)]
pub mod tests {
    use ::rocket::local::blocking::Client;
    use common::*;
    use rstest::rstest;
    use tempfile::TempDir;

    /// Contains reusable code which can be shared across different tests, like response assertions,
    /// fixtures to set up the test environment and particular test implementations
    /// which can be seeded with different values, but the implementation scheme remains the same.
    pub mod common {
        use crate::app::configure_rocket;
        use ::rocket::http::ContentType;
        use ::rocket::local::blocking::Client;
        use rstest::fixture;
        use sequoia_openpgp::Cert;
        use sequoia_openpgp::cert::CertBuilder;
        use sequoia_openpgp::serialize::Serialize;
        use std::path::PathBuf;
        use tempfile::{TempDir, tempdir};

        /// Fake base URI to use in tests.
        #[fixture]
        pub fn base_uri() -> &'static str {
            "http://local.connection"
        }

        #[fixture]
        pub fn base_uri_onion() -> &'static str {
            "http://local.connection.onion"
        }

        #[fixture]
        pub fn cert_name() -> &'static str {
            "foo@invalid.example.com"
        }

        #[fixture]
        pub fn alt_cert_name() -> &'static str {
            "bar@invalid.example.com"
        }

        #[fixture]
        pub fn cert<'a>(cert_name: &'a str) -> (&'a str, Cert) {
            let (tpk, _) = CertBuilder::new()
                .add_signing_subkey()
                .add_transport_encryption_subkey()
                .add_userid(cert_name)
                .generate()
                .unwrap();

            (cert_name, tpk)
        }

        #[fixture]
        pub fn serialized_cert<'a>(
            #[from(cert)] (cert_name, tpk): (&'a str, Cert),
        ) -> (&'a str, Vec<u8>) {
            let mut tpk_serialized = Vec::new();
            tpk.serialize(&mut tpk_serialized).unwrap();

            (cert_name, tpk_serialized)
        }

        #[fixture]
        pub fn cert_v6<'a>(cert_name: &'a str) -> (&'a str, Cert) {
            let (tpk, _) = CertBuilder::new()
                .set_profile(sequoia_openpgp::Profile::RFC9580)
                .unwrap()
                .add_signing_subkey()
                .add_transport_encryption_subkey()
                .add_userid(cert_name)
                .generate()
                .unwrap();

            (cert_name, tpk)
        }

        #[fixture]
        pub fn serialized_cert_v6<'a>(
            #[from(cert_v6)] (cert_name, tpk): (&'a str, Cert),
        ) -> (&'a str, Vec<u8>) {
            let mut tpk_serialized = Vec::new();
            tpk.serialize(&mut tpk_serialized).unwrap();

            (cert_name, tpk_serialized)
        }

        #[fixture]
        pub fn armored_cert_v5() -> &'static str {
            "mEkFaOIaiBYAAAA/AytlcQHIoNJUMEagn3vY3TVcjmk6IwPT6XGqZxrZWDLni2l1S+sUvlRo7GjDN6t5JiAh55AKfdZSu5dCGEwAtBRsaWJyZXBncEBleGFtcGxlLm9yZ4jHBRMWCgBHIiEFNPltsdPkQvVM5+Wy8wqpHnriOKnA+ojWuLPqPAsfHocFAmjiGogCGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AAAA03Acjzt2u/8rNUHPow1N/KotfNGnNbjR8ilFxuwRG0Khr6FB4tiLPhdsePEmSl5ucxDPCnIMV/FyXyzoAByLjoTbWQWWc6C+hNv23Q/DZy9BYDjCc4Vx/LPiSiwv69XR3vFiIeYs6/lKu5pvUmNB+s1+nuBuAdALhMBWjiGogSAAAAQgMrZW8Bv3C9iXJpY4SVgy13d5V08wP6lfjP6mJmdt8CVsetXZwlohsPgowHtYXZWUWJO+PSm9abMCQu3juPAwEKCYisBRgWCgAsIiEFNPltsdPkQvVM5+Wy8wqpHnriOKnA+ojWuLPqPAsfHocFAmjiGogCGwwAAAk7AcjHNFOgiNNKuklZNMqyWbpnCehc/eSOHIjO7jwuByY9Gj56oT5xY7amIG4F6ahpF1IToA9tm5JIuYABxRp7PBnc7co9IiDydP3MCEjY0sKktmZS24DsavsR4eI1+l1+7cZXr6Nv3kLssgRxxgTVhgy36noHAA=="
        }

        #[fixture]
        pub fn key_multipart_form_data(
            #[from(serialized_cert)] (_cert_name, serialized_cert): (&str, Vec<u8>),
        ) -> (ContentType, Vec<u8>) {
            let ct = ContentType::new("multipart", "form-data").with_params((
                "boundary",
                "---------------------------14733842173518794281682249499",
            ));

            let header = b"-----------------------------14733842173518794281682249499\r\n\
              Content-Disposition: form-data; name=\"csrf\"\r\n\
              \r\n\
              \r\n\
              -----------------------------14733842173518794281682249499\r\n\
              Content-Disposition: form-data; name=\"keytext\"; filename=\".k\"\r\n\
              Content-Type: application/octet-stream\r\n\
              \r\n";
            let footer = b"\r\n-----------------------------14733842173518794281682249499--";

            let mut body = Vec::new();
            body.extend_from_slice(header);
            body.extend_from_slice(&serialized_cert);
            body.extend_from_slice(footer);

            (ct, body)
        }

        /// Creates a configuration and empty state dir for testing purposes.
        ///
        /// Note that you need to keep the returned TempDir alive for the
        /// duration of your test.  To debug the test, mem::forget it to
        /// prevent cleanup.
        #[fixture]
        pub fn configuration(
            base_uri: &str,
            base_uri_onion: &str,
        ) -> (TempDir, ::rocket::figment::Figment) {
            let root = tempdir().expect("valid temporary directory for configuration");

            let filemail = root.path().join("filemail");
            ::std::fs::create_dir_all(&filemail).expect("valid temporary directory for filemail");

            let base_dir: PathBuf = root.path().into();

            let config = ::rocket::Config::figment()
                .select("staging")
                .merge(("root", root.path()))
                .merge((
                    "template_dir",
                    ::std::env::current_dir()
                        .unwrap()
                        .join("dist/templates")
                        .to_str()
                        .unwrap(),
                ))
                .merge((
                    "email_template_dir",
                    ::std::env::current_dir()
                        .unwrap()
                        .join("dist/email-templates")
                        .to_str()
                        .unwrap(),
                ))
                .merge((
                    "assets_dir",
                    ::std::env::current_dir()
                        .unwrap()
                        .join("dist/assets")
                        .to_str()
                        .unwrap(),
                ))
                .merge((
                    "keys_internal_dir",
                    base_dir.join("keys_internal").to_str().unwrap(),
                ))
                .merge((
                    "keys_external_dir",
                    base_dir.join("keys_external").to_str().unwrap(),
                ))
                .merge(("tmp_dir", base_dir.join("tmp").to_str().unwrap()))
                .merge(("token_dir", base_dir.join("tokens").to_str().unwrap()))
                .merge((
                    "maintenance_file",
                    base_dir.join("maintenance").to_str().unwrap(),
                ))
                .merge(("base-URI", base_uri))
                .merge(("base-URI-Onion", base_uri_onion))
                .merge(("from", "from@example.com"))
                .merge(("token_secret", "hagrid"))
                .merge(("token_validity", 3600u64))
                .merge((
                    "filemail_into",
                    filemail
                        .into_os_string()
                        .into_string()
                        .expect("path is valid UTF8"),
                ));

            (root, config)
        }

        #[fixture]
        fn rocket(
            #[from(configuration)] (tmpdir, config): (TempDir, ::rocket::figment::Figment),
        ) -> (TempDir, ::rocket::Rocket<::rocket::Build>) {
            (tmpdir, configure_rocket(::rocket::custom(config)))
        }

        #[fixture]
        pub fn client(
            #[from(rocket)] (tmpdir, rocket): (TempDir, ::rocket::Rocket<::rocket::Build>),
        ) -> (TempDir, Client) {
            let client = Client::untracked(rocket).expect("valid rocket instance");

            (tmpdir, client)
        }

        #[fixture]
        pub fn maintenance_text() -> &'static str {
            "maintenance-message"
        }

        #[fixture]
        pub fn token() -> &'static str {
            "token-pseudo-random"
        }

        /// Contains reusable assertion helpers.
        pub mod assert {
            use ::rocket::http::{ContentType, Status};
            use ::rocket::local::blocking::LocalResponse;
            use hagrid_database::{Database, Sqlite};

            pub fn consistency(rocket: &::rocket::Rocket<::rocket::Orbit>) {
                let db = rocket.state::<Sqlite>().unwrap();
                db.check_consistency().unwrap();
            }

            pub fn response(
                response: LocalResponse,
                status: Status,
                content_type: ContentType,
                present_page_text: &str,
            ) {
                assert_eq!(response.status(), status);
                assert_eq!(response.content_type(), Some(content_type));

                let body = response.into_string().unwrap();
                println!("{body}");
                assert!(body.contains(present_page_text));
            }

            pub fn response_does_not_contain_text(
                response: LocalResponse,
                status: Status,
                content_type: ContentType,
                absent_page_text: &str,
            ) {
                assert_eq!(response.status(), status);
                assert_eq!(response.content_type(), Some(content_type));

                let body = response.into_string().unwrap();
                println!("{body}");
                assert!(!body.contains(absent_page_text));
            }

            pub mod maintenance {
                use super::*;

                pub fn response(
                    response: LocalResponse,
                    content_type: ContentType,
                    maintenance_text: &str,
                ) {
                    super::response(
                        response,
                        Status::ServiceUnavailable,
                        content_type,
                        maintenance_text,
                    );
                }
            }
        }

        /// Contains reusable test implementations.
        pub mod test {
            use super::*;
            use ::rocket::http::{ContentType, Status};
            use ::rocket::local::blocking::LocalResponse;
            use std::fs;
            use std::fs::File;
            use std::io::Write;
            use tempfile::TempDir;

            /// NOTE: As dispatch_request closure called twice data referenced by closure has to be
            /// cloned instead of moved into.
            pub fn maintenance<'c>(
                dispatch_request: impl Fn() -> LocalResponse<'c>,
                tmpdir: TempDir,
                content_type: ContentType,
                maintenance_text: &str,
            ) {
                let maintenance_path = tmpdir.path().join("maintenance");
                let mut file = File::create(&maintenance_path).unwrap();
                file.write_all(maintenance_text.as_bytes()).unwrap();

                // Check that endpoint return a maintenance message
                assert::maintenance::response(
                    dispatch_request(),
                    content_type.clone(),
                    maintenance_text,
                );

                fs::remove_file(&maintenance_path).unwrap();

                assert::response_does_not_contain_text(
                    dispatch_request(),
                    Status::Ok,
                    content_type,
                    maintenance_text,
                );
            }
        }
    }

    #[rstest]
    fn basic_consistency(#[from(client)] (_tmpdir, client): (TempDir, Client)) {
        assert::consistency(client.rocket());
    }
}
