use crate::extract::AuthLogger;
use crate::{app, DevApi, EternaltwinSystem};
use axum::body::BoxBody;
use axum::http::header::{COOKIE, SET_COOKIE};
use axum::http::{HeaderValue, Request, Response, StatusCode};
use axum::{Extension, Router};
use cookie_store::{Cookie, CookieStore};
use eternaltwin_auth_store::mem::MemAuthStore;
use eternaltwin_config::Config;
use eternaltwin_core::auth::{AuthStore, RegisterWithUsernameOptions};
use eternaltwin_core::clock::{Clock, ErasedScheduler, Scheduler, VirtualClock};
use eternaltwin_core::core::Instant;
use eternaltwin_core::dinoparc::DinoparcStore;
use eternaltwin_core::email::{EmailFormatter, Mailer};
use eternaltwin_core::forum::ForumStore;
use eternaltwin_core::hammerfest::HammerfestStore;
use eternaltwin_core::job::{JobRuntime, JobStore, OpaqueTask, OpaqueValue};
use eternaltwin_core::link::{LinkStore, VersionedLinks};
use eternaltwin_core::oauth::OauthProviderStore;
use eternaltwin_core::password::{Password, PasswordService};
use eternaltwin_core::twinoid::TwinoidStore;
use eternaltwin_core::user::{User, UserDisplayNameVersion, UserDisplayNameVersions, UserStore};
use eternaltwin_core::uuid::{Uuid4Generator, UuidGenerator};
use eternaltwin_dinoparc_store::mem::MemDinoparcStore;
use eternaltwin_email_formatter::json::JsonEmailFormatter;
use eternaltwin_forum_store::mem::MemForumStore;
use eternaltwin_hammerfest_store::mem::MemHammerfestStore;
use eternaltwin_job_store::mem::MemJobStore;
use eternaltwin_link_store::mem::MemLinkStore;
use eternaltwin_log::NoopLogger;
use eternaltwin_mailer::mem::MemMailer;
use eternaltwin_oauth_provider_store::mem::MemOauthProviderStore;
use eternaltwin_password::scrypt::ScryptPasswordService;
use eternaltwin_services::auth::AuthService;
use eternaltwin_services::dinoparc::DinoparcService;
use eternaltwin_services::forum::ForumService;
use eternaltwin_services::hammerfest::HammerfestService;
use eternaltwin_services::job::JobService;
use eternaltwin_services::oauth::OauthService;
use eternaltwin_services::twinoid::TwinoidService;
use eternaltwin_services::user::UserService;
use eternaltwin_twinoid_store::mem::MemTwinoidStore;
use eternaltwin_user_store::mem::MemUserStore;
use hyper::Body;
use std::convert::Infallible;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tower::ServiceExt;

pub(crate) fn create_api() -> EternaltwinSystem {
  let config = Config::for_test();
  let mut dev = DevApi::new();
  let clock = Arc::new(VirtualClock::new(Instant::ymd_hms(2020, 1, 1, 0, 0, 0)));
  dev.clock = Some(Arc::clone(&clock));
  let hammerfest_store: Arc<dyn HammerfestStore> = Arc::new(MemHammerfestStore::new(Arc::clone(&clock)));
  let dinoparc_store: Arc<dyn DinoparcStore> = Arc::new(MemDinoparcStore::new(Arc::clone(&clock)));
  let forum_store: Arc<dyn ForumStore> = Arc::new(MemForumStore::new(Arc::clone(&clock), Uuid4Generator));
  let job_store: Arc<dyn JobStore<OpaqueTask, OpaqueValue>> = Arc::new(MemJobStore::new(Uuid4Generator));
  let link_store: Arc<dyn LinkStore> = Arc::new(MemLinkStore::new(Arc::clone(&clock)));
  let user_store: Arc<dyn UserStore> = Arc::new(MemUserStore::new(Arc::clone(&clock), Uuid4Generator));
  let auth_store: Arc<dyn AuthStore> = Arc::new(MemAuthStore::new(Arc::clone(&clock), Uuid4Generator));
  let email_formatter: Arc<dyn EmailFormatter> = Arc::new(JsonEmailFormatter);
  let mailer: Arc<dyn Mailer> = Arc::new(MemMailer::new());
  let password: Arc<dyn PasswordService> = Arc::new(ScryptPasswordService::with_os_rng(
    config.scrypt.max_time.value,
    config.scrypt.max_mem_frac.value,
  ));
  let oauth_provider_store: Arc<dyn OauthProviderStore> = Arc::new(MemOauthProviderStore::new(
    Arc::clone(&clock),
    Arc::clone(&password),
    Uuid4Generator,
  ));
  let twinoid_store: Arc<dyn TwinoidStore> = Arc::new(MemTwinoidStore::new(Arc::clone(&clock)));
  let internal_auth_key = b"dev_secret_internal_auth".to_vec();
  let auth_secret = b"dev_secret".to_vec();

  let auth = Arc::new(AuthService::new(
    Arc::clone(&auth_store),
    Arc::clone(&clock) as Arc<dyn Clock>,
    Arc::clone(&dinoparc_store),
    email_formatter,
    Arc::clone(&hammerfest_store),
    Arc::clone(&link_store),
    mailer,
    Arc::clone(&oauth_provider_store),
    Arc::clone(&password),
    Arc::clone(&user_store),
    Arc::clone(&twinoid_store),
    Arc::new(Uuid4Generator) as Arc<dyn UuidGenerator>,
    internal_auth_key,
    auth_secret,
  ));

  let dinoparc = Arc::new(DinoparcService::new(
    Arc::clone(&dinoparc_store),
    Arc::clone(&link_store),
    Arc::clone(&user_store),
  ));

  let forum = Arc::new(ForumService::new(
    Arc::clone(&clock) as Arc<dyn Clock>,
    Arc::clone(&forum_store),
    Arc::clone(&user_store),
  ));

  let hammerfest = Arc::new(HammerfestService::new(
    Arc::clone(&hammerfest_store),
    Arc::clone(&link_store),
    Arc::clone(&user_store),
  ));

  let oauth = Arc::new(OauthService::new(
    Arc::clone(&oauth_provider_store),
    Arc::clone(&user_store),
  ));

  let twinoid = Arc::new(TwinoidService::new(
    Arc::clone(&clock) as Arc<dyn Clock>,
    Arc::clone(&link_store),
    Arc::clone(&twinoid_store),
    Arc::clone(&user_store),
  ));

  let user = Arc::new(UserService::new(
    Arc::clone(&auth_store),
    Arc::clone(&clock) as Arc<dyn Clock>,
    Arc::clone(&dinoparc_store),
    Arc::clone(&hammerfest_store),
    Arc::clone(&link_store),
    password,
    Arc::clone(&twinoid_store),
    Arc::clone(&user_store),
  ));

  let scheduler = Arc::new(ErasedScheduler::new(Arc::clone(&clock)))
    as Arc<dyn Scheduler<Timer = Pin<Box<dyn Future<Output = ()> + Send>>>>;

  let job_runtime = Arc::new(JobRuntime::new(
    Arc::clone(&scheduler),
    Arc::clone(&job_store),
    Arc::clone(&hammerfest_store),
    Arc::clone(&twinoid_store),
  ));

  let job = Arc::new(JobService::new(
    Arc::clone(&scheduler),
    Arc::clone(&job_runtime),
    Arc::clone(&job_store),
    Arc::clone(&user_store),
  ));

  EternaltwinSystem {
    dev,
    auth,
    clock: scheduler,
    dinoparc,
    forum,
    hammerfest,
    job,
    job_runtime,
    oauth,
    twinoid,
    user,
  }
}

#[tokio::test]
async fn test_empty_hammerfest_user() {
  let api = create_api();
  let router = app(api);

  let req = axum::http::Request::builder()
    .method("GET")
    .uri("/api/v1/archive/hammerfest/hammerfest.fr/users/123")
    .body(axum::body::Body::empty())
    .unwrap();

  let res = router.client().send(req).await;
  assert_eq!(res.status(), 404);
  let body = hyper::body::to_bytes(res.into_body())
    .await
    .expect("read body")
    .to_vec();

  // let body: &str = std::str::from_utf8(res.body()).unwrap();
  assert_eq!(body.as_slice(), b"{\"error\":\"HammerfestUserNotFound\"}");
}

#[tokio::test]
async fn test_auth() {
  let api = create_api();
  let logger = AuthLogger(Arc::new(NoopLogger));
  let router = app(api).layer(Extension(logger));

  let req = axum::http::Request::builder()
    .method("GET")
    .uri("/api/v1/auth/self")
    .body(axum::body::Body::empty())
    .unwrap();

  let res = router.client().send(req).await;
  assert_eq!(res.status(), 200);
  let body = hyper::body::to_bytes(res.into_body())
    .await
    .expect("read body")
    .to_vec();

  let body: &str = std::str::from_utf8(body.as_slice()).unwrap();
  assert_eq!(body, r#"{"type":"Guest","scope":"Default"}"#);
}

#[tokio::test]
async fn test_create_user() {
  let api = create_api();
  let logger = AuthLogger(Arc::new(NoopLogger));
  let router = app(api).layer(Extension(logger));
  let mut client = router.client();

  let req = axum::http::Request::builder()
    .method("POST")
    .uri("/api/v1/users")
    .header("Content-Type", "application/json")
    .body(axum::body::Body::from(
      serde_json::to_string(&RegisterWithUsernameOptions {
        username: "alice".parse().unwrap(),
        display_name: "Alice".parse().unwrap(),
        password: Password::from("aaaaaaaaaa"),
      })
      .unwrap(),
    ))
    .unwrap();

  let res = client.send(req).await;
  assert_eq!(res.status(), StatusCode::OK);
  let body = hyper::body::to_bytes(res.into_body())
    .await
    .expect("read body")
    .to_vec();

  let body: &str = std::str::from_utf8(body.as_slice()).unwrap();
  let actual: User = serde_json::from_str(body).unwrap();
  let expected = User {
    id: actual.id,
    created_at: Instant::ymd_hms(2020, 1, 1, 0, 0, 0),
    deleted_at: None,
    display_name: UserDisplayNameVersions {
      current: UserDisplayNameVersion {
        value: "Alice".parse().unwrap(),
      },
    },
    is_administrator: true,
    links: VersionedLinks::default(),
  };
  assert_eq!(actual, expected);
}

pub(crate) struct Client {
  router: Router,
  cookies: CookieStore,
}

impl Client {
  pub fn new(router: Router) -> Self {
    Self {
      router,
      cookies: CookieStore::default(),
    }
  }

  pub async fn send(&mut self, mut req: Request<Body>) -> Response<BoxBody> {
    let mut req_url = url::Url::parse("http://eternaltwin.localhost/").expect("base uri");
    req_url.set_path(req.uri().path());
    let cookies = self
      .cookies
      .get_request_values(&req_url)
      .map(|(name, value)| format!("{}={}", name, value))
      .collect::<Vec<_>>()
      .join("; ");
    if !cookies.is_empty() {
      req
        .headers_mut()
        .insert(COOKIE, HeaderValue::from_str(&cookies).expect("invalid cookies"));
    }
    let res: Result<_, Infallible> = self.router.clone().with_state(()).oneshot(req).await;
    let res = match res {
      Ok(r) => r,
      Err(_) => unreachable!("`Infaillible` error"),
    };
    for cookie in res.headers().get_all(SET_COOKIE) {
      let cookie: Cookie = Cookie::parse(cookie.to_str().expect("invalid cookie"), &req_url).expect("parse cookie");
      self.cookies.insert_raw(&cookie, &req_url).expect("cookie insertion");
    }
    res
  }
}

pub(crate) trait RouterExt {
  fn client(&self) -> Client;
}

impl RouterExt for Router {
  fn client(&self) -> Client {
    Client::new(self.clone())
  }
}
