use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;

use hydracache::{CacheKeyBuilder, HydraCache, TagSet};
use serde::{Deserialize, Serialize};

use crate::{CacheEntity, DbCache, DbCacheError, HydraCacheEntity, QueryCachePolicy};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, HydraCacheEntity)]
#[hydracache(entity = "user", collection = "users", id = u64)]
struct User {
    id: u64,
    name: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, HydraCacheEntity)]
#[hydracache(
    entity = "account:user",
    collection = "users:active",
    id = &'static str
)]
struct AccountUser {
    id: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, HydraCacheEntity)]
#[hydracache(entity = "profile", id = u64)]
struct Profile {
    id: u64,
}

#[derive(Debug)]
struct LoadError;

impl std::fmt::Display for LoadError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str("load failed")
    }
}

impl std::error::Error for LoadError {}

fn adapter() -> DbCache {
    DbCache::new(HydraCache::local().build(), "db")
}

#[tokio::test]
async fn fetch_with_requires_explicit_key() {
    let result = adapter()
        .cached::<User>()
        .fetch_with(|| async { Ok::<_, LoadError>(user(1)) })
        .await;

    assert!(matches!(
        result,
        Err(DbCacheError::MissingKey { operation }) if operation == "db:unnamed"
    ));
}

#[tokio::test]
async fn query_builder_exposes_metadata() {
    let query = adapter()
        .named::<User>("load-user")
        .key_builder(CacheKeyBuilder::new().tenant(7).entity("user", 42))
        .tag("users")
        .tags(["user:42", "tenant:7"])
        .ttl(Duration::from_secs(30));

    assert_eq!(query.namespace(), "db");
    assert_eq!(query.name(), Some("load-user"));
    assert_eq!(query.key_value(), Some("tenant:7:user:42"));
    assert_eq!(query.physical_key(), Some("db:tenant:7:user:42".to_owned()));
    assert_eq!(
        query.tags_value(),
        &[
            "users".to_owned(),
            "user:42".to_owned(),
            "tenant:7".to_owned()
        ]
    );
    assert_eq!(query.ttl_value(), Some(Duration::from_secs(30)));
}

#[tokio::test]
async fn query_cache_policy_exposes_reusable_metadata() {
    let policy = QueryCachePolicy::named("load-user")
        .key_builder(CacheKeyBuilder::new().tenant(7).entity("user", 42))
        .tag("users")
        .tags(["user:42", "tenant:7"])
        .ttl(Duration::from_secs(30));

    assert_eq!(policy.name(), Some("load-user"));
    assert_eq!(policy.key_value(), Some("tenant:7:user:42"));
    assert_eq!(
        policy.tags_value(),
        &[
            "users".to_owned(),
            "user:42".to_owned(),
            "tenant:7".to_owned()
        ]
    );
    assert_eq!(policy.ttl_value(), Some(Duration::from_secs(30)));
}

#[tokio::test]
async fn cached_with_applies_reusable_query_cache_policy() {
    let policy = QueryCachePolicy::named("load-user")
        .for_cache_entity::<User>(42)
        .ttl(Duration::from_secs(30));

    let first = adapter().cached_with::<User>(policy.clone());
    let second = adapter().cached_with::<User>(policy);

    assert_eq!(first.name(), Some("load-user"));
    assert_eq!(first.key_value(), Some("user:42"));
    assert_eq!(
        first.tags_value(),
        &["user:42".to_owned(), "users".to_owned()]
    );
    assert_eq!(first.ttl_value(), Some(Duration::from_secs(30)));
    assert_eq!(second.physical_key(), Some("db:user:42".to_owned()));
}

#[tokio::test]
async fn with_policy_replaces_existing_descriptor_policy() {
    let policy = QueryCachePolicy::new().key("new").tag("new-tag");
    let query = adapter()
        .cached::<User>()
        .key("old")
        .tag("old-tag")
        .with_policy(policy);

    assert_eq!(query.key_value(), Some("new"));
    assert_eq!(query.tags_value(), &["new-tag".to_owned()]);
    assert_eq!(query.cache_policy().key_value(), Some("new"));
}

#[tokio::test]
async fn query_cache_policy_collection_sets_key_and_tag() {
    let policy = QueryCachePolicy::new().collection("users:active");
    let query = adapter().cached_with::<Vec<User>>(policy);

    assert_eq!(query.key_value(), Some("users%3Aactive"));
    assert_eq!(query.physical_key(), Some("db:users%3Aactive".to_owned()));
    assert_eq!(query.tags_value(), &["users%3Aactive".to_owned()]);
}

#[tokio::test]
async fn entity_helper_sets_escaped_key_and_entity_tag() {
    let query = adapter().entity::<User>("user:type", "42%beta");

    assert_eq!(query.key_value(), Some("user%3Atype:42%25beta"));
    assert_eq!(
        query.physical_key(),
        Some("db:user%3Atype:42%25beta".to_owned())
    );
    assert_eq!(query.tags_value(), &["user%3Atype:42%25beta".to_owned()]);
}

#[tokio::test]
async fn collection_helper_sets_escaped_key_and_collection_tag() {
    let query = adapter().collection::<User>("users:active");

    assert_eq!(query.key_value(), Some("users%3Aactive"));
    assert_eq!(query.physical_key(), Some("db:users%3Aactive".to_owned()));
    assert_eq!(query.tags_value(), &["users%3Aactive".to_owned()]);
}

#[tokio::test]
async fn descriptor_collection_method_sets_escaped_key_and_collection_tag() {
    let query = adapter().cached::<User>().collection("users:active");

    assert_eq!(query.key_value(), Some("users%3Aactive"));
    assert_eq!(query.physical_key(), Some("db:users%3Aactive".to_owned()));
    assert_eq!(query.tags_value(), &["users%3Aactive".to_owned()]);
}

#[tokio::test]
async fn for_entity_replaces_key_and_preserves_existing_tags() {
    let query = adapter()
        .cached::<User>()
        .key("old")
        .tag("existing")
        .for_entity("user", 42)
        .collection_tag("users");

    assert_eq!(query.key_value(), Some("user:42"));
    assert_eq!(
        query.tags_value(),
        &[
            "existing".to_owned(),
            "user:42".to_owned(),
            "users".to_owned()
        ]
    );
}

#[tokio::test]
async fn collection_tag_escapes_collection_segment() {
    let query = adapter()
        .entity::<User>("user", 42)
        .collection_tag("users:active");

    assert_eq!(
        query.tags_value(),
        &["user:42".to_owned(), "users%3Aactive".to_owned()]
    );
}

#[tokio::test]
async fn cache_entity_trait_generates_default_metadata() {
    assert_eq!(User::cache_key_for(&42), "user:42");
    assert_eq!(User::entity_tag_for(&42), "user:42");
    assert_eq!(User::collection_tag(), Some("users".to_owned()));
}

#[tokio::test]
async fn cache_entity_helper_sets_key_entity_tag_and_collection_tag() {
    let query = adapter().for_entity::<User>(42);

    assert_eq!(query.key_value(), Some("user:42"));
    assert_eq!(query.physical_key(), Some("db:user:42".to_owned()));
    assert_eq!(
        query.tags_value(),
        &["user:42".to_owned(), "users".to_owned()]
    );
}

#[tokio::test]
async fn cache_entity_helper_escapes_entity_id_and_collection_segments() {
    let query = adapter().for_entity::<AccountUser>("42%beta");

    assert_eq!(query.key_value(), Some("account%3Auser:42%25beta"));
    assert_eq!(
        query.tags_value(),
        &[
            "account%3Auser:42%25beta".to_owned(),
            "users%3Aactive".to_owned()
        ]
    );
}

#[tokio::test]
async fn cache_entity_without_collection_only_adds_entity_tag() {
    let query = adapter().for_entity::<Profile>(7);

    assert_eq!(query.key_value(), Some("profile:7"));
    assert_eq!(query.tags_value(), &["profile:7".to_owned()]);
}

#[tokio::test]
async fn query_for_cache_entity_preserves_existing_tags() {
    let query = adapter()
        .cached::<User>()
        .tag("tenant:7")
        .for_cache_entity(42);

    assert_eq!(query.key_value(), Some("user:42"));
    assert_eq!(
        query.tags_value(),
        &[
            "tenant:7".to_owned(),
            "user:42".to_owned(),
            "users".to_owned()
        ]
    );
}

#[tokio::test]
async fn explicit_key_can_override_generated_entity_key() {
    let query = adapter().entity::<User>("user", 42).key("custom:user:42");

    assert_eq!(query.key_value(), Some("custom:user:42"));
    assert_eq!(query.physical_key(), Some("db:custom:user:42".to_owned()));
    assert_eq!(query.tags_value(), &["user:42".to_owned()]);
}

#[tokio::test]
async fn entity_helper_caches_loaded_value_and_uses_generated_tag() {
    let calls = Arc::new(AtomicUsize::new(0));
    let cache = adapter();

    let first = cache
        .entity::<User>("user", 1)
        .collection_tag("users")
        .fetch_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(user(1))
            }
        })
        .await
        .unwrap();

    let cached = cache
        .entity::<User>("user", 1)
        .collection_tag("users")
        .fetch_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(user(2))
            }
        })
        .await
        .unwrap();

    assert_eq!(first, user(1));
    assert_eq!(cached, user(1));
    assert_eq!(calls.load(Ordering::SeqCst), 1);

    assert_eq!(cache.cache().invalidate_tag("user:1").await.unwrap(), 1);

    let reloaded = cache
        .entity::<User>("user", 1)
        .fetch_with(|| async { Ok::<_, LoadError>(user(2)) })
        .await
        .unwrap();

    assert_eq!(reloaded, user(2));
}

#[tokio::test]
async fn collection_helper_caches_adapter_chosen_output_type() {
    let calls = Arc::new(AtomicUsize::new(0));
    let cache = adapter();

    let first: Vec<User> = cache
        .collection::<User>("users")
        .fetch_value_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(vec![user(1)])
            }
        })
        .await
        .unwrap();

    let cached: Vec<User> = cache
        .collection::<User>("users")
        .fetch_value_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(vec![user(2)])
            }
        })
        .await
        .unwrap();

    assert_eq!(first, vec![user(1)]);
    assert_eq!(cached, vec![user(1)]);
    assert_eq!(calls.load(Ordering::SeqCst), 1);

    assert_eq!(cache.cache().invalidate_tag("users").await.unwrap(), 1);
}

#[tokio::test]
async fn cache_entity_helper_caches_and_invalidates_by_collection_tag() {
    let calls = Arc::new(AtomicUsize::new(0));
    let cache = adapter();

    let first = cache
        .for_entity::<User>(1)
        .fetch_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(user(1))
            }
        })
        .await
        .unwrap();

    let cached = cache
        .for_entity::<User>(1)
        .fetch_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(user(2))
            }
        })
        .await
        .unwrap();

    assert_eq!(first, user(1));
    assert_eq!(cached, user(1));
    assert_eq!(calls.load(Ordering::SeqCst), 1);

    assert_eq!(cache.cache().invalidate_tag("users").await.unwrap(), 1);

    let reloaded = cache
        .for_entity::<User>(1)
        .fetch_with(|| async { Ok::<_, LoadError>(user(2)) })
        .await
        .unwrap();

    assert_eq!(reloaded, user(2));
}

#[tokio::test]
async fn query_builder_with_name_replaces_diagnostic_label() {
    let query = adapter()
        .cached::<User>()
        .with_name("load-user")
        .key("user:1");

    assert_eq!(adapter().namespace(), "db");
    assert_eq!(query.name(), Some("load-user"));
}

#[tokio::test]
async fn adapter_and_query_derived_impls_are_usable() {
    let cache = adapter();
    let cache_clone = cache.clone();
    let query = cache.cached::<User>().key("user:1").clone();

    assert_eq!(cache_clone.namespace(), "db");
    assert!(format!("{cache:?}").contains("DbCache"));
    assert!(format!("{query:?}").contains("DbQuery"));
}

#[tokio::test]
async fn fetch_with_caches_loaded_value() {
    let calls = Arc::new(AtomicUsize::new(0));
    let cache = adapter();

    let first = cache
        .cached::<User>()
        .key("user:1")
        .fetch_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(user(1))
            }
        })
        .await
        .unwrap();

    let second = cache
        .cached::<User>()
        .key("user:1")
        .fetch_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(user(2))
            }
        })
        .await
        .unwrap();

    assert_eq!(first, user(1));
    assert_eq!(second, user(1));
    assert_eq!(calls.load(Ordering::SeqCst), 1);
}

#[tokio::test]
async fn load_alias_caches_repository_result() {
    let calls = Arc::new(AtomicUsize::new(0));
    let cache = adapter();

    let first = cache
        .for_entity::<User>(1)
        .load({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(user(1))
            }
        })
        .await
        .unwrap();

    let cached = cache
        .for_entity::<User>(1)
        .load({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(user(2))
            }
        })
        .await
        .unwrap();

    assert_eq!(first, user(1));
    assert_eq!(cached, user(1));
    assert_eq!(calls.load(Ordering::SeqCst), 1);
}

#[tokio::test]
async fn tag_invalidation_removes_cached_query_result() {
    let cache = adapter();

    cache
        .cached::<User>()
        .key("user:1")
        .tag_set(TagSet::new().tag("users").entity("user", 1))
        .fetch_with(|| async { Ok::<_, LoadError>(user(1)) })
        .await
        .unwrap();

    assert_eq!(cache.cache().invalidate_tag("user:1").await.unwrap(), 1);

    let reloaded = cache
        .cached::<User>()
        .key("user:1")
        .fetch_with(|| async { Ok::<_, LoadError>(user(2)) })
        .await
        .unwrap();

    assert_eq!(reloaded, user(2));
}

#[tokio::test]
async fn per_query_ttl_expires_cached_query_result() {
    let cache = adapter();

    cache
        .cached::<User>()
        .key("user:ttl")
        .ttl(Duration::from_millis(20))
        .fetch_with(|| async { Ok::<_, LoadError>(user(1)) })
        .await
        .unwrap();

    tokio::time::sleep(Duration::from_millis(40)).await;

    let reloaded = cache
        .cached::<User>()
        .key("user:ttl")
        .fetch_with(|| async { Ok::<_, LoadError>(user(2)) })
        .await
        .unwrap();

    assert_eq!(reloaded, user(2));
}

#[tokio::test]
async fn empty_namespace_uses_logical_key_as_physical_key() {
    let query = DbCache::new(HydraCache::local().build(), "")
        .cached::<User>()
        .key("one");

    assert_eq!(query.physical_key(), Some("one".to_owned()));
}

#[tokio::test]
async fn query_as_keeps_sql_text_as_diagnostic_name() {
    let query = adapter()
        .query_as::<User>("select id from users")
        .key("users");

    assert_eq!(query.name(), Some("select id from users"));
}

#[tokio::test]
async fn missing_key_error_uses_available_context() {
    let result = adapter()
        .named::<User>("load-profile")
        .fetch_with(|| async { Ok::<_, LoadError>(user(1)) })
        .await;

    assert!(matches!(
        result,
        Err(DbCacheError::MissingKey { operation }) if operation == "load-profile"
    ));
}

#[tokio::test]
async fn missing_key_error_uses_key_context_for_unnamed_queries() {
    let result = DbCache::new(HydraCache::local().build(), "")
        .cached::<User>()
        .fetch_with(|| async { Ok::<_, LoadError>(user(1)) })
        .await;

    assert!(matches!(
        result,
        Err(DbCacheError::MissingKey { operation }) if operation == "unnamed"
    ));

    let result = adapter()
        .cached::<User>()
        .fetch_with(|| async { Ok::<_, LoadError>(user(1)) })
        .await;

    assert!(matches!(
        result,
        Err(DbCacheError::MissingKey { operation }) if operation == "db:unnamed"
    ));

    let result = DbCache::new(HydraCache::local().build(), "db")
        .cached::<User>()
        .with_name("")
        .fetch_with(|| async { Ok::<_, LoadError>(user(1)) })
        .await;

    assert!(matches!(
        result,
        Err(DbCacheError::MissingKey { operation }) if operation.is_empty()
    ));
}

#[tokio::test]
async fn fetch_value_with_caches_adapter_chosen_output_type() {
    let calls = Arc::new(AtomicUsize::new(0));
    let cache = adapter();

    let first: Option<User> = cache
        .cached::<User>()
        .key("maybe-user:1")
        .fetch_value_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(Some(user(1)))
            }
        })
        .await
        .unwrap();

    let second: Option<User> = cache
        .cached::<User>()
        .key("maybe-user:1")
        .fetch_value_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(Some(user(2)))
            }
        })
        .await
        .unwrap();

    assert_eq!(first, Some(user(1)));
    assert_eq!(second, Some(user(1)));
    assert_eq!(calls.load(Ordering::SeqCst), 1);
}

#[tokio::test]
async fn fetch_value_with_caches_empty_vectors() {
    let calls = Arc::new(AtomicUsize::new(0));
    let cache = adapter();

    let first: Vec<User> = cache
        .cached::<User>()
        .key("users:none")
        .fetch_value_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(Vec::new())
            }
        })
        .await
        .unwrap();

    let second: Vec<User> = cache
        .cached::<User>()
        .key("users:none")
        .fetch_value_with({
            let calls = Arc::clone(&calls);
            move || async move {
                calls.fetch_add(1, Ordering::SeqCst);
                Ok::<_, LoadError>(vec![user(2)])
            }
        })
        .await
        .unwrap();

    assert!(first.is_empty());
    assert!(second.is_empty());
    assert_eq!(calls.load(Ordering::SeqCst), 1);
}

#[tokio::test]
async fn fetch_value_with_requires_explicit_key() {
    let result: crate::Result<Option<User>> = adapter()
        .cached::<User>()
        .fetch_value_with(|| async { Ok::<_, LoadError>(None) })
        .await;

    assert!(matches!(
        result,
        Err(DbCacheError::MissingKey { operation }) if operation == "db:unnamed"
    ));
}

fn user(id: u64) -> User {
    User {
        id,
        name: format!("user-{id}"),
    }
}
