// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use std::{net::SocketAddr, path::Path, sync::Arc};

use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use ordinary_storage::saferlmdb::{self, EnvBuilder};
use sha2::{Digest, Sha256};
use tokio::net::TcpListener;

use ordinary_auth::{Auth, AuthClient};

use ordinary_app::{self, server::OrdinaryAppServer};
use ordinary_config::{AuthConfig, OrdinaryApiLimits, OrdinaryConfig, StorageLimits};

use ordinary_utils::shutdown_signal;
use std::error::Error;

async fn server() -> Result<SocketAddr, Box<dyn Error>> {
    if tokio_rustls::rustls::crypto::ring::default_provider()
        .install_default()
        .is_err()
    {
        tracing::error!("failed to get rustls default provider");
    }

    let config = OrdinaryConfig {
        domain: "example.com".into(),
        cnames: None,
        version: "0.1.0".into(),
        contacts: vec![],
        storage_size: 10_000_000,
        default_timeout: None,
        csp: None,
        cors: None,
        runtime: None,
        hide_schema: None,
        client_rendering: None,
        obfuscation: None,
        client_events: None,
        port: Some(8080),
        redirect_port: None,
        logging: None,
        error: None,
        auth: Some(AuthConfig {
            ..Default::default()
        }),
        globals: None,
        secrets: None,
        models: None,
        integrations: None,
        actions: None,
        assets: None,
        templates: None,
        content: None,
        flags: None,
    };

    let listener = TcpListener::bind("0.0.0.0:0").await?;
    let addr = listener.local_addr()?;

    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 limits = OrdinaryApiLimits::default();

    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(
        config.domain.clone(),
        config.auth.clone(),
        [0u8; 32],
        env.clone(),
    )?);

    let storage = Arc::new(ordinary_storage::Storage::new(
        StorageLimits::default(),
        if let Some(models) = &config.models {
            models.clone()
        } else {
            vec![]
        },
        if let Some(content) = &config.content {
            content.definitions.clone()
        } else {
            vec![]
        },
        [0u8; 32],
        &env,
        store_dir.join("search"),
        false,
    )?);

    let app = OrdinaryAppServer::new(
        config, limits, auth, storage, false, false, false, false, None, None, None,
    )?;

    tokio::spawn(async move {
        app.start::<&str, _>(
            listener,
            None,
            ordinary_app::server::SecurityMode::Insecure,
            None,
            shutdown_signal,
        )
        .await
        .expect("server crashed");
    });

    Ok(addr)
}

#[tokio::test]
async fn integration() -> Result<(), Box<dyn Error>> {
    let addr = server().await?;
    let client = reqwest::Client::new();

    // !! register (wasm)

    let (state, reg_start_req) = AuthClient::registration_start_req(b"account", b"password")?;

    let reg_start_res = client
        .put(format!("http://{addr}/accounts/registration/start"))
        .body(reg_start_req)
        .send()
        .await?
        .bytes()
        .await?;

    let (private_key, reg_finish_req) =
        AuthClient::registration_finish_req(b"account", b"password", &state, &reg_start_res)?;

    let reg_finish_res = client
        .put(format!("http://{addr}/accounts/registration/finish"))
        .body(reg_finish_req)
        .send()
        .await?
        .bytes()
        .await?;

    let (mfa_code, recovery_codes) = AuthClient::decrypt_totp_mfa_code(
        &reg_finish_res,
        private_key,
        "test".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 (wasm)

    let (state, login_start_req) = AuthClient::login_start_req(b"account", b"password")?;
    let login_start_res = client
        .put(format!("http://{addr}/accounts/login/start"))
        .body(login_start_req)
        .send()
        .await?
        .bytes()
        .await?;

    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();

    // ?? without client signature

    let (login_finish_req, session_key) = AuthClient::login_finish_req(
        b"account",
        b"password",
        &mfa_hash,
        &state,
        &login_start_res,
        None,
    )?;
    let login_finish_res = client
        .put(format!("http://{addr}/accounts/login/finish"))
        .body(login_finish_req)
        .send()
        .await?
        .bytes()
        .await?;

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

    // !! get access (without cookies)

    let _access_token = client
        .put(format!("http://{addr}/accounts/access/get"))
        .body(refresh_token)
        .send()
        .await?
        .bytes()
        .await?;

    // !! reset password

    // login start
    let (state, req) = AuthClient::reset_password_login_start_req(b"account", b"password")?;
    let res = client
        .put(format!("http://{addr}/accounts/password/reset/login/start"))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    // login finish
    let (req, session_key) = AuthClient::reset_password_login_finish_req(
        b"account",
        b"password",
        &mfa_hash,
        &state,
        &res,
        None,
    )?;
    let res = client
        .put(format!(
            "http://{addr}/accounts/password/reset/login/finish"
        ))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    let password_reset_token = AuthClient::decrypt_token(&res, &session_key)?;
    let b64_password_reset_token = b64.encode(password_reset_token);

    // re-register start
    let (state, req) = AuthClient::password_reset_registration_start_req(b"account", b"password2")?;
    let res = client
        .put(format!(
            "http://{addr}/accounts/password/reset/registration/start"
        ))
        .body(req)
        .header(
            "authorization",
            format!("Bearer {b64_password_reset_token}"),
        )
        .send()
        .await?
        .bytes()
        .await?;

    // re-register finish
    let req =
        AuthClient::password_reset_registration_finish_req(b"account", b"password2", &state, &res)?;
    client
        .put(format!(
            "http://{addr}/accounts/password/reset/registration/finish"
        ))
        .body(req)
        .header(
            "authorization",
            format!("Bearer {b64_password_reset_token}"),
        )
        .send()
        .await?
        .status();

    // login start
    let (state, login_start_req) = AuthClient::login_start_req(b"account", b"password2")?;
    let login_start_res = client
        .put(format!("http://{addr}/accounts/login/start"))
        .body(login_start_req)
        .send()
        .await?
        .bytes()
        .await?;

    // login finish
    let (login_finish_req, _) = AuthClient::login_finish_req(
        b"account",
        b"password2",
        &mfa_hash,
        &state,
        &login_start_res,
        None,
    )?;
    client
        .put(format!("http://{addr}/accounts/login/finish"))
        .body(login_finish_req)
        .send()
        .await?
        .status();

    // !! 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 = client
        .put(format!("http://{addr}/accounts/password/forgot/start"))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    // forgot finish
    let req = AuthClient::forgot_password_finish_req(
        b"account",
        b"password3",
        &state,
        &res,
        recovery_code1.as_bytes(),
    )?;
    client
        .put(format!("http://{addr}/accounts/password/forgot/finish"))
        .body(req)
        .send()
        .await?
        .status();

    // login start
    let (state, req) = AuthClient::login_start_req(b"account", b"password3")?;
    let res = client
        .put(format!("http://{addr}/accounts/login/start"))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    // login finish
    let (req, _) =
        AuthClient::login_finish_req(b"account", b"password3", &mfa_hash, &state, &res, None)?;
    client
        .put(format!("http://{addr}/accounts/login/finish"))
        .body(req)
        .send()
        .await?
        .status();

    // !! reset totp mfa

    // mfa reset start
    let (state, req) = AuthClient::reset_totp_mfa_start_req(b"account", b"password3")?;
    let res = client
        .put(format!("http://{addr}/accounts/mfa/totp/reset/start"))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    // mfa reset finish
    let (req, session_key) =
        AuthClient::reset_totp_mfa_finish_req(b"account", b"password3", &mfa_hash, &state, &res)?;
    let res = client
        .put(format!("http://{addr}/accounts/mfa/totp/reset/finish"))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    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 = client
        .put(format!("http://{addr}/accounts/login/start"))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    // login 2 finish
    let (req, _) =
        AuthClient::login_finish_req(b"account", b"password3", &mfa_hash2, &state, &res, None)?;
    client
        .put(format!("http://{addr}/accounts/login/finish"))
        .body(req)
        .send()
        .await?
        .status();

    // !! 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 = client
        .put(format!("http://{addr}/accounts/mfa/totp/lost/start"))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    // 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 = client
        .put(format!("http://{addr}/accounts/mfa/totp/lost/finish"))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    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 = client
        .put(format!("http://{addr}/accounts/login/start"))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    // login 3 finish
    let (req, _) =
        AuthClient::login_finish_req(b"account", b"password3", &mfa_hash3, &state, &res, None)?;
    client
        .put(format!("http://{addr}/accounts/login/finish"))
        .body(req)
        .send()
        .await?
        .status();

    // !! reset recovery codes

    // reset recovery codes start
    let (state, req) = AuthClient::reset_recovery_codes_start_req(b"account", b"password3")?;
    let res = client
        .put(format!("http://{addr}/accounts/recovery-codes/reset/start"))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    // reset recovery codes finish
    let (req, session_key) = AuthClient::reset_recovery_codes_finish_req(
        b"account",
        b"password3",
        &mfa_hash3,
        &state,
        &res,
    )?;
    let res = client
        .put(format!(
            "http://{addr}/accounts/recovery-codes/reset/finish"
        ))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    // 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 = client
        .put(format!("http://{addr}/accounts/password/forgot/start"))
        .body(req)
        .send()
        .await?
        .bytes()
        .await?;

    // forgot finish
    let req = AuthClient::forgot_password_finish_req(
        b"account",
        b"password4",
        &state,
        &res,
        recovery_code3.as_bytes(),
    )?;
    client
        .put(format!("http://{addr}/accounts/password/forgot/finish"))
        .body(req)
        .send()
        .await?
        .status();

    // todo: more testing

    Ok(())
}
