use std::{
    error::Error,
    path::Path,
    sync::Arc,
    time::{Duration, SystemTime},
};

use bytes::{BufMut, BytesMut};
use cbwaw::{Auth, AuthClient, OsRng};
use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut};
use ordinary_config::{AuthConfig, InviteConfig, InviteMode};
use saferlmdb::EnvBuilder;
use sha2::{Digest, Sha256};

#[test]
fn integration() -> Result<(), Box<dyn Error>> {
    let store_dir = Path::new(".ordinary").join("store");

    if std::fs::read_dir(&store_dir).is_ok() {
        std::fs::remove_dir_all(&store_dir)?;
    }

    std::fs::create_dir_all(&store_dir)?;

    let env = Arc::new(unsafe {
        let mut env_builder = EnvBuilder::new()?;
        env_builder.set_maxreaders(126)?;
        env_builder.set_mapsize(16384 * 64 * 10)?;
        env_builder.set_maxdbs(13)?;
        env_builder.open(
            store_dir.to_str().expect("store dir is not str"),
            &saferlmdb::open::Flags::empty(),
            0o600,
        )?
    });

    let auth = Arc::new(Auth::new(
        "example.com".into(),
        None,
        [0u8; 32],
        env.clone(),
    )?);

    // !! register

    // registration start
    let (state, req) = AuthClient::registration_start_req(b"account", b"password", None)?;
    let res = auth.registration_start(req, None, None)?;

    // registration finish
    let (private_key, req) =
        AuthClient::registration_finish_req(b"account", b"password", &state, &res)?;
    let (res, ..) = auth.registration_finish(req, None)?;

    let (mfa_code, recovery_codes) = AuthClient::decrypt_totp_mfa_code(
        &res,
        private_key,
        "example.com".into(),
        "account".into(),
    )?;

    let mut recovery_code1 = String::new();
    let mut recovery_code2 = String::new();

    for (i, c) in recovery_codes.chars().enumerate() {
        if i < 11 {
            recovery_code1 = format!("{recovery_code1}{c}");
        } else if i < 22 {
            recovery_code2 = format!("{recovery_code2}{c}");
        } else {
            break;
        }
    }

    // !! login

    // login start
    let (state, req) = AuthClient::login_start_req(b"account", b"password")?;
    let res = auth.login_start(req)?;

    // client signing keys
    let mut signing_key: SigningKey = SigningKey::generate(&mut OsRng);
    let verifying_key = signing_key.verifying_key();

    let mut mfa_input = b"example.com".to_vec();
    mfa_input.extend_from_slice(b"account");
    mfa_input.extend_from_slice(mfa_code.as_bytes());

    let mut hasher = Sha256::new();
    hasher.update(&mfa_input);
    let mfa_hash = hasher.finalize().to_vec();

    // login finish
    let (req, session_key) = AuthClient::login_finish_req(
        b"account",
        b"password",
        &mfa_hash,
        &state,
        &res,
        Some(verifying_key.as_bytes()),
    )?;
    let (res, _, _) = auth.login_finish(req, true)?;

    let mut refresh_token: BytesMut = AuthClient::decrypt_token(&res, &session_key)?.into();

    // !! tokens with client signature

    // client exp
    let exp = SystemTime::now()
        .checked_add(Duration::from_secs(5))
        .expect("time to work")
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs() as u32;

    refresh_token.put_u32(exp);

    let signature = signing_key.sign(&refresh_token[..]);

    refresh_token.put(&signature.to_bytes()[..]);

    // get access
    let mut access_token: BytesMut = auth.access_get(refresh_token.into())?.into();

    // client exp
    let exp = SystemTime::now()
        .checked_add(Duration::from_secs(3))
        .expect("time to work")
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs() as u32;

    access_token.put_u32(exp);

    let signature = signing_key.sign(&access_token[..]);

    access_token.put(&signature.to_bytes()[..]);

    let (_account, claims) = auth.verify_access_token(&access_token[..])?;

    assert_eq!(claims.idx(0).as_vector().idx(0).as_blob().0.len(), 16);

    // !! tokens without client signature

    // login start
    let (state, req) = AuthClient::login_start_req(b"account", b"password")?;
    let res = auth.login_start(req)?;

    // login finish
    let (req, session_key) =
        AuthClient::login_finish_req(b"account", b"password", &mfa_hash, &state, &res, None)?;
    let (res, _, _) = auth.login_finish(req, true)?;

    let refresh_token = AuthClient::decrypt_token(&res, &session_key)?;

    // get access
    let access_token = auth.access_get(refresh_token)?;

    let (_account, claims) = auth.verify_access_token(&access_token[..])?;

    assert_eq!(claims.idx(0).as_vector().idx(0).as_blob().0.len(), 16);

    // !! reset password

    // reset login start
    let (state, req) = AuthClient::reset_password_login_start_req(b"account", b"password")?;
    let res = auth.reset_password_login_start(req)?;

    // reset login finish
    let (req, session_key) = AuthClient::reset_password_login_finish_req(
        b"account",
        b"password",
        &mfa_hash,
        &state,
        &res,
        None,
    )?;
    let res = auth.reset_password_login_finish(req)?;

    let password_reset_token = AuthClient::decrypt_token(&res, &session_key)?;

    // reset re-register start
    let (state, req) = AuthClient::password_reset_registration_start_req(b"account", b"password2")?;
    let res = auth.reset_password_registration_start(req, &password_reset_token)?;

    // reset re-register finish
    let req =
        AuthClient::password_reset_registration_finish_req(b"account", b"password2", &state, &res)?;
    auth.reset_password_registration_finish(req, &password_reset_token)?;

    // login start
    let (state, req) = AuthClient::login_start_req(b"account", b"password2")?;
    let res = auth.login_start(req)?;

    // login finish
    let (req, _) =
        AuthClient::login_finish_req(b"account", b"password2", &mfa_hash, &state, &res, None)?;
    auth.login_finish(req, true)?;

    // !! forgot password

    // forgot start
    let mut hasher = Sha256::new();
    hasher.update(recovery_code1.as_bytes());
    let hashed_recovery_code1 = hasher.finalize().to_vec();

    let (state, req) =
        AuthClient::forgot_password_start_req(b"account", b"password3", &hashed_recovery_code1)?;
    let res = auth.forgot_password_start(req)?;

    // forgot finish
    let req = AuthClient::forgot_password_finish_req(
        b"account",
        b"password3",
        &state,
        &res,
        recovery_code1.as_bytes(),
    )?;
    auth.forgot_password_finish(req)?;

    // login start
    let (state, req) = AuthClient::login_start_req(b"account", b"password3")?;
    let res = auth.login_start(req)?;

    // login finish
    let (req, _) =
        AuthClient::login_finish_req(b"account", b"password3", &mfa_hash, &state, &res, None)?;
    auth.login_finish(req, true)?;

    // !! reset totp mfa

    // mfa reset start
    let (state, req) = AuthClient::reset_totp_mfa_start_req(b"account", b"password3")?;
    let res = auth.reset_totp_mfa_start(req)?;

    // mfa reset finish
    let (req, session_key) =
        AuthClient::reset_totp_mfa_finish_req(b"account", b"password3", &mfa_hash, &state, &res)?;
    let res = auth.reset_totp_mfa_finish(req)?;

    let mfa_code2 = AuthClient::decrypt_reset_totp_mfa_code(
        &res,
        &session_key,
        "example.com".into(),
        "account".into(),
    )?;

    let mut mfa_input2 = b"example.com".to_vec();
    mfa_input2.extend_from_slice(b"account");
    mfa_input2.extend_from_slice(mfa_code2.as_bytes());

    let mut hasher2 = Sha256::new();
    hasher2.update(&mfa_input2);
    let mfa_hash2 = hasher2.finalize().to_vec();

    // login 2 start
    let (state, req) = AuthClient::login_start_req(b"account", b"password3")?;
    let res = auth.login_start(req)?;

    // login 2 finish
    let (req, _) =
        AuthClient::login_finish_req(b"account", b"password3", &mfa_hash2, &state, &res, None)?;
    auth.login_finish(req, true)?;

    // !! lost totp mfa

    // mfa lost start
    let mut hasher = Sha256::new();
    hasher.update(recovery_code2.as_bytes());
    let hashed_recovery_code2 = hasher.finalize().to_vec();

    let (state, req) =
        AuthClient::lost_totp_mfa_start_req(b"account", b"password3", &hashed_recovery_code2)?;
    let res = auth.lost_totp_mfa_start(req)?;

    // mfa lost finish
    let (req, session_key) = AuthClient::lost_totp_mfa_finish_req(
        b"account",
        b"password3",
        &state,
        &res,
        recovery_code2.as_bytes(),
    )?;
    let res = auth.lost_totp_mfa_finish(req)?;

    let mfa_code3 = AuthClient::decrypt_lost_totp_mfa_code(
        &res,
        &session_key,
        "example.com".into(),
        "account".into(),
    )?;

    let mut mfa_input3 = b"example.com".to_vec();
    mfa_input3.extend_from_slice(b"account");
    mfa_input3.extend_from_slice(mfa_code3.as_bytes());

    let mut hasher3 = Sha256::new();
    hasher3.update(&mfa_input3);
    let mfa_hash3 = hasher3.finalize().to_vec();

    // login 3 start
    let (state, req) = AuthClient::login_start_req(b"account", b"password3")?;
    let res = auth.login_start(req)?;

    // login 3 finish
    let (req, _) =
        AuthClient::login_finish_req(b"account", b"password3", &mfa_hash3, &state, &res, None)?;
    auth.login_finish(req, true)?;

    // !! reset recovery codes

    // reset recovery codes start
    let (state, req) = AuthClient::reset_recovery_codes_start_req(b"account", b"password3")?;
    let res = auth.reset_recovery_codes_start(req)?;

    // reset recovery codes finish
    let (req, session_key) = AuthClient::reset_recovery_codes_finish_req(
        b"account",
        b"password3",
        &mfa_hash3,
        &state,
        &res,
    )?;
    let res = auth.reset_recovery_codes_finish(req)?;

    // extract recovery code
    let recovery_codes = AuthClient::decrypt_reset_recovery_codes(&res, &session_key)?;

    let mut recovery_code3 = String::new();

    for (i, c) in recovery_codes.chars().enumerate() {
        if i < 11 {
            recovery_code3 = format!("{recovery_code3}{c}");
        } else {
            break;
        }
    }

    // forgot start
    let mut hasher = Sha256::new();
    hasher.update(recovery_code3.as_bytes());
    let hashed_recovery_code3 = hasher.finalize().to_vec();

    let (state, req) =
        AuthClient::forgot_password_start_req(b"account", b"password4", &hashed_recovery_code3)?;
    let res = auth.forgot_password_start(req)?;

    // forgot finish
    let req = AuthClient::forgot_password_finish_req(
        b"account",
        b"password4",
        &state,
        &res,
        recovery_code3.as_bytes(),
    )?;
    auth.forgot_password_finish(req)?;

    // !! delete account

    // delete account start
    let (state, req) = AuthClient::delete_account_start_req(b"account", b"password4")?;
    let res = auth.delete_account_start(req)?;

    // delete account finish
    let req =
        AuthClient::delete_account_finish_req(b"account", b"password4", &mfa_hash3, &state, &res)?;
    auth.delete_account_finish(req)?;

    drop(auth);
    drop(env);

    // !! invite codes

    let env = Arc::new(unsafe {
        let mut env_builder = EnvBuilder::new()?;
        env_builder.set_maxreaders(126)?;
        env_builder.set_mapsize(16384 * 64 * 10)?;
        env_builder.set_maxdbs(13)?;
        env_builder.open(
            store_dir.to_str().expect("store dir is not str"),
            &saferlmdb::open::Flags::empty(),
            0o600,
        )?
    });

    let auth = Arc::new(Auth::new(
        "example.com".into(),
        Some(AuthConfig {
            invite: Some(InviteConfig {
                mode: InviteMode::Viral,
                ..Default::default()
            }),
            ..Default::default()
        }),
        [0u8; 32],
        env.clone(),
    )?);

    let invite_code = auth.admin_invite_get("admin.example.com", "root", None)?;

    // registration start
    let (state, req) =
        AuthClient::registration_start_req(b"account", b"password", Some(invite_code))?;
    let res = auth.registration_start(req, None, None)?;

    // registration finish
    let (private_key, req) =
        AuthClient::registration_finish_req(b"account", b"password", &state, &res)?;
    let (res, ..) = auth.registration_finish(req, None)?;

    AuthClient::decrypt_totp_mfa_code(&res, private_key, "example.com".into(), "account".into())?;

    Ok(())
}
