use crate::dev::hammerfest_client::UpsertHammerfestUserBody;
use crate::extract::AuthLogger;
use crate::{app, DevApi, RouterApi};
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::CookieStore;
use etwin_auth_store::mem::MemAuthStore;
use etwin_core::auth::{AuthStore, RegisterWithUsernameOptions};
use etwin_core::clock::{Clock, VirtualClock};
use etwin_core::core::{Instant, UserDot};
use etwin_core::dinoparc::{DinoparcClient, DinoparcStore};
use etwin_core::email::{EmailFormatter, Mailer};
use etwin_core::forum::ForumStore;
use etwin_core::hammerfest::{HammerfestClient, HammerfestServer, HammerfestStore, ShortHammerfestUser};
use etwin_core::link::{Link, LinkStore, VersionedLink, VersionedLinks};
use etwin_core::oauth::OauthProviderStore;
use etwin_core::password::{Password, PasswordService};
use etwin_core::token::TokenStore;
use etwin_core::twinoid::{TwinoidClient, TwinoidStore};
use etwin_core::user::{
  LinkToHammerfestOptions, LinkToHammerfestWithCredentialsOptions, ShortUser, User, UserDisplayNameVersion,
  UserDisplayNameVersions, UserStore,
};
use etwin_core::uuid::{Uuid4Generator, UuidGenerator};
use etwin_dinoparc_client::mem::MemDinoparcClient;
use etwin_dinoparc_store::mem::MemDinoparcStore;
use etwin_email_formatter::json::JsonEmailFormatter;
use etwin_forum_store::mem::MemForumStore;
use etwin_hammerfest_client::MemHammerfestClient;
use etwin_hammerfest_store::mem::MemHammerfestStore;
use etwin_link_store::mem::MemLinkStore;
use etwin_log::NoopLogger;
use etwin_mailer::mem::MemMailer;
use etwin_oauth_provider_store::mem::MemOauthProviderStore;
use etwin_password::scrypt::ScryptPasswordService;
use etwin_services::auth::AuthService;
use etwin_services::dinoparc::DinoparcService;
use etwin_services::forum::ForumService;
use etwin_services::hammerfest::HammerfestService;
use etwin_services::twinoid::TwinoidService;
use etwin_services::user::UserService;
use etwin_token_store::mem::MemTokenStore;
use etwin_twinoid_client::mem::MemTwinoidClient;
use etwin_twinoid_store::mem::MemTwinoidStore;
use etwin_user_store::mem::MemUserStore;
use hyper::Body;
use std::convert::Infallible;
use std::sync::Arc;
use tower::ServiceExt;

pub(crate) fn create_api() -> RouterApi {
  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_client = Arc::new(MemHammerfestClient::new(Arc::clone(&clock) as Arc<dyn Clock>));
  dev.hammerfest_client = Some(Arc::clone(&hammerfest_client));
  let hammerfest_client: Arc<dyn HammerfestClient> = hammerfest_client;
  let hammerfest_store: Arc<dyn HammerfestStore> = Arc::new(MemHammerfestStore::new(Arc::clone(&clock)));
  let dinoparc_client: Arc<dyn DinoparcClient> = Arc::new(MemDinoparcClient::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 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::recommended_for_tests());
  let oauth_provider_store: Arc<dyn OauthProviderStore> = Arc::new(MemOauthProviderStore::new(
    Arc::clone(&clock),
    Arc::clone(&password),
    Uuid4Generator,
  ));
  let token_store: Arc<dyn TokenStore> = Arc::new(MemTokenStore::new(Arc::clone(&clock)));
  let twinoid_client: Arc<dyn TwinoidClient> = Arc::new(MemTwinoidClient);
  let twinoid_store: Arc<dyn TwinoidStore> = Arc::new(MemTwinoidStore::new(Arc::clone(&clock)));
  let auth_secret = b"dev_secret".to_vec();

  let auth = Arc::new(AuthService::new(
    auth_store,
    Arc::clone(&clock) as Arc<dyn Clock>,
    Arc::clone(&dinoparc_client),
    Arc::clone(&dinoparc_store),
    email_formatter,
    Arc::clone(&hammerfest_client),
    Arc::clone(&hammerfest_store),
    Arc::clone(&link_store),
    mailer,
    oauth_provider_store,
    Arc::clone(&password),
    Arc::clone(&user_store),
    Arc::clone(&twinoid_client),
    Arc::clone(&twinoid_store),
    Arc::new(Uuid4Generator) as Arc<dyn UuidGenerator>,
    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_client),
    Arc::clone(&hammerfest_store),
    Arc::clone(&link_store),
    Arc::clone(&user_store),
  ));

  let twinoid = Arc::new(TwinoidService::new(
    Arc::clone(&link_store),
    Arc::clone(&twinoid_store),
    Arc::clone(&user_store),
  ));

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

  RouterApi {
    dev,
    auth,
    clock,
    dinoparc,
    forum,
    hammerfest,
    twinoid,
    user,
  }
}

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

  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, None).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, None).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),
    display_name: UserDisplayNameVersions {
      current: UserDisplayNameVersion {
        value: "Alice".parse().unwrap(),
      },
    },
    is_administrator: true,
    links: VersionedLinks::default(),
  };
  assert_eq!(actual, expected);
}

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

  {
    let req = axum::http::Request::builder()
      .method("PUT")
      .uri("/api/v1/hammerfest_client/users")
      .header("Content-Type", "application/json")
      .body(axum::body::Body::from(
        serde_json::to_string(&UpsertHammerfestUserBody {
          server: HammerfestServer::HammerfestFr,
          id: "123".parse().unwrap(),
          username: "alicehf".parse().unwrap(),
          password: "alicehf".parse().unwrap(),
        })
        .unwrap(),
      ))
      .unwrap();

    let res = client.send(req).await;
    let actual_status = res.status();
    assert_eq!(actual_status, StatusCode::OK);
  }
  let alice = {
    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();
    serde_json::from_str::<User>(body).unwrap()
  };
  {
    let req = axum::http::Request::builder()
      .method("PUT")
      .uri(format!("/api/v1/users/{}/links/hammerfest.fr", alice.id))
      .header("Content-Type", "application/json")
      .body(axum::body::Body::from(
        serde_json::to_string(&LinkToHammerfestOptions::Credentials(
          LinkToHammerfestWithCredentialsOptions {
            user_id: alice.id,
            hammerfest_server: HammerfestServer::HammerfestFr,
            hammerfest_username: "alicehf".parse().unwrap(),
            hammerfest_password: "alicehf".parse().unwrap(),
          },
        ))
        .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: VersionedLink<ShortHammerfestUser> = serde_json::from_str(body).unwrap();
    let expected = VersionedLink {
      current: Some(Link {
        link: UserDot {
          time: Instant::ymd_hms(2020, 1, 1, 0, 0, 0),
          user: ShortUser {
            id: alice.id,
            display_name: UserDisplayNameVersions {
              current: UserDisplayNameVersion {
                value: "Alice".parse().unwrap(),
              },
            },
          },
        },
        unlink: (),
        remote: ShortHammerfestUser {
          server: HammerfestServer::HammerfestFr,
          id: "123".parse().unwrap(),
          username: "alicehf".parse().unwrap(),
        },
      }),
      old: vec![],
    };
    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().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")).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())
  }
}
