use tranquil_pds::delegation::{ValidatedDelegationScope, intersect_scopes};
use tranquil_pds::oauth::scopes::{
    AccountAction, IdentityAttr, ParsedScope, RepoAction, ScopePermissions, parse_scope,
    parse_scope_string,
};
use tranquil_types::Nsid;

fn c(s: &str) -> Nsid {
    Nsid::new(s).expect("valid NSID")
}

#[test]
fn test_repo_star_defaults_to_all_actions() {
    let scope = parse_scope("repo:*");
    if let ParsedScope::Repo(repo) = scope {
        assert!(repo.actions.contains(&RepoAction::Create));
        assert!(repo.actions.contains(&RepoAction::Update));
        assert!(repo.actions.contains(&RepoAction::Delete));
        assert_eq!(repo.actions.len(), 3);
    } else {
        panic!("Expected Repo scope");
    }
}

#[test]
fn test_repo_collection_without_actions_defaults_to_all() {
    let scope = parse_scope("repo:app.bsky.feed.post");
    if let ParsedScope::Repo(repo) = scope {
        assert!(repo.actions.contains(&RepoAction::Create));
        assert!(repo.actions.contains(&RepoAction::Update));
        assert!(repo.actions.contains(&RepoAction::Delete));
    } else {
        panic!("Expected Repo scope");
    }
}

#[test]
fn test_repo_empty_string_after_colon() {
    let scope = parse_scope("repo:");
    if let ParsedScope::Repo(repo) = scope {
        assert!(repo.collection.is_none());
    } else {
        panic!("Expected Repo scope");
    }
}

#[test]
fn test_rpc_wildcard_aud_wildcard_forbidden() {
    let scope = parse_scope("rpc:*?aud=*");
    assert!(matches!(scope, ParsedScope::Unknown(_)));
}

#[test]
fn test_rpc_no_lxm_aud_wildcard_forbidden() {
    let scope = parse_scope("rpc?aud=*");
    assert!(matches!(scope, ParsedScope::Unknown(_)));
}

#[test]
fn test_rpc_specific_lxm_wildcard_aud_allowed() {
    let scope = parse_scope("rpc:app.bsky.feed.getTimeline?aud=*");
    assert!(matches!(scope, ParsedScope::Rpc(_)));
}

#[test]
fn test_rpc_wildcard_lxm_specific_aud_allowed() {
    let scope = parse_scope("rpc:*?aud=did:web:api.bsky.app");
    assert!(matches!(scope, ParsedScope::Rpc(_)));
}

#[test]
fn test_unknown_scope_preserved() {
    let scope = parse_scope("completely:made:up:scope");
    if let ParsedScope::Unknown(s) = scope {
        assert_eq!(s, "completely:made:up:scope");
    } else {
        panic!("Expected Unknown scope");
    }
}

#[test]
fn test_unknown_scope_with_params_preserved() {
    let scope = parse_scope("unknown:thing?param=value");
    if let ParsedScope::Unknown(s) = scope {
        assert_eq!(s, "unknown:thing?param=value");
    } else {
        panic!("Expected Unknown scope");
    }
}

#[test]
fn test_blob_empty_accept() {
    let scope = parse_scope("blob");
    if let ParsedScope::Blob(blob) = scope {
        assert!(blob.accept.is_empty());
        assert!(blob.matches_mime("anything/goes"));
    } else {
        panic!("Expected Blob scope");
    }
}

#[test]
fn test_blob_matches_wildcard() {
    let scope = parse_scope("blob:*/*");
    if let ParsedScope::Blob(blob) = scope {
        assert!(blob.matches_mime("image/png"));
        assert!(blob.matches_mime("video/mp4"));
        assert!(blob.matches_mime("application/json"));
    } else {
        panic!("Expected Blob scope");
    }
}

#[test]
fn test_blob_type_prefix_matching() {
    let scope = parse_scope("blob:image/*");
    if let ParsedScope::Blob(blob) = scope {
        assert!(blob.matches_mime("image/png"));
        assert!(blob.matches_mime("image/jpeg"));
        assert!(blob.matches_mime("image/gif"));
        assert!(!blob.matches_mime("video/mp4"));
        assert!(!blob.matches_mime("images/png"));
    } else {
        panic!("Expected Blob scope");
    }
}

#[test]
fn test_account_default_action_is_read() {
    let scope = parse_scope("account:email");
    if let ParsedScope::Account(a) = scope {
        assert_eq!(a.action, AccountAction::Read);
    } else {
        panic!("Expected Account scope");
    }
}

#[test]
fn test_multiple_scopes_parsing() {
    let scopes = parse_scope_string("atproto repo:* blob:*/* transition:generic");
    assert_eq!(scopes.len(), 4);
    assert!(matches!(scopes[0], ParsedScope::Atproto));
}

#[test]
fn test_permissions_null_scope_defaults_atproto() {
    let perms = ScopePermissions::from_scope_string(None);
    assert!(!perms.has_full_access());
    assert!(!perms.allows_repo(RepoAction::Create, &c("cafe.oyster.record")));
    assert!(!perms.allows_repo(RepoAction::Update, &c("cafe.oyster.record")));
    assert!(!perms.allows_repo(RepoAction::Delete, &c("cafe.oyster.record")));
}

#[test]
fn test_permissions_empty_string_defaults_atproto() {
    let perms = ScopePermissions::from_scope_string(Some(""));
    assert!(!perms.has_full_access());
}

#[test]
fn test_permissions_whitespace_only() {
    let perms = ScopePermissions::from_scope_string(Some("   "));
    assert!(!perms.has_full_access());
}

#[test]
fn test_permissions_repo_collection_wildcard_prefix() {
    let perms = ScopePermissions::from_scope_string(Some("repo:app.bsky.*?action=create"));
    assert!(perms.allows_repo(RepoAction::Create, &c("app.bsky.feed.post")));
    assert!(perms.allows_repo(RepoAction::Create, &c("app.bsky.actor.profile")));
    assert!(!perms.allows_repo(RepoAction::Create, &c("com.atproto.repo.blob")));
    assert!(!perms.allows_repo(RepoAction::Update, &c("app.bsky.feed.post")));
}

#[test]
fn test_permissions_rpc_lxm_wildcard_prefix() {
    let perms =
        ScopePermissions::from_scope_string(Some("rpc:app.bsky.feed.*?aud=did:web:api.bsky.app"));
    assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")));
    assert!(perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getAuthorFeed")));
    assert!(!perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.actor.getProfile")));
}

#[test]
fn test_permissions_rpc_aud_service_id_must_match_verbatim() {
    let perms =
        ScopePermissions::from_scope_string(Some("rpc:app.bsky.feed.*?aud=did:web:api.bsky.app"));
    assert!(
        perms.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")),
        "the granted audience must cover itself"
    );
    assert!(
        !perms.allows_rpc(
            "did:web:api.bsky.app#bsky_appview",
            &c("app.bsky.feed.getTimeline")
        ),
        "a bare DID grants nothing to the services listed under it"
    );
    assert!(
        !perms.allows_rpc(
            "did:web:other.example#bsky_appview",
            &c("app.bsky.feed.getTimeline")
        ),
        "a service id must not smuggle in a different audience"
    );

    let fragment_scope = ScopePermissions::from_scope_string(Some(
        "rpc:app.bsky.feed.*?aud=did:web:api.bsky.app%23bsky_appview",
    ));
    assert!(
        fragment_scope.allows_rpc(
            "did:web:api.bsky.app#bsky_appview",
            &c("app.bsky.feed.getTimeline")
        ),
        "the granted service id must cover itself"
    );
    assert!(
        !fragment_scope.allows_rpc("did:web:api.bsky.app", &c("app.bsky.feed.getTimeline")),
        "a scope granted for one service must not widen to the whole DID"
    );
    assert!(
        !fragment_scope.allows_rpc(
            "did:web:api.bsky.app#atproto_labeler",
            &c("app.bsky.feed.getTimeline")
        ),
        "the appview and the labeler are different audiences even on one DID"
    );
}

#[test]
fn test_delegation_intersect_mismatched_params_empty() {
    let result = intersect_scopes("repo:*?action=create", "repo:*?action=delete");
    assert!(
        result.is_empty(),
        "Mismatched action params must produce empty intersection, got: '{}'",
        result
    );
}

#[test]
fn test_delegation_intersect_wildcard_vs_specific() {
    let result = intersect_scopes("repo:app.bsky.feed.post?action=create", "repo:*");
    assert_eq!(
        result, "repo:app.bsky.feed.post?action=create",
        "Intersection must return the narrower requested scope, not the granted wildcard"
    );
}

#[test]
fn test_delegation_validate_known_prefixes() {
    assert!(ValidatedDelegationScope::new("atproto").is_ok());
    assert!(ValidatedDelegationScope::new("repo:*").is_ok());
    assert!(ValidatedDelegationScope::new("blob:*/*").is_ok());
    assert!(ValidatedDelegationScope::new("rpc:*").is_ok());
    assert!(ValidatedDelegationScope::new("account:email").is_ok());
    assert!(ValidatedDelegationScope::new("identity:handle").is_ok());
    assert!(ValidatedDelegationScope::new("transition:generic").is_ok());
}

#[test]
fn test_delegation_validate_unknown_prefixes() {
    assert!(ValidatedDelegationScope::new("invalid:scope").is_err());
    assert!(ValidatedDelegationScope::new("custom:something").is_err());
    assert!(ValidatedDelegationScope::new("made:up").is_err());
}

#[test]
fn test_delegation_validate_empty() {
    assert!(ValidatedDelegationScope::new("").is_ok());
}

#[test]
fn test_delegation_validate_multiple() {
    assert!(ValidatedDelegationScope::new("atproto repo:* blob:*/*").is_ok());
    assert!(ValidatedDelegationScope::new("atproto invalid:scope").is_err());
}

#[test]
fn test_delegation_intersect_empty_grant_keeps_only_atproto() {
    assert_eq!(intersect_scopes("atproto", ""), "atproto");
    assert_eq!(intersect_scopes("repo:*", ""), "");
}

#[test]
fn test_delegation_intersect_no_overlap() {
    let result = intersect_scopes("repo:app.bsky.feed.post", "repo:com.atproto.something");
    assert!(result.is_empty());
}

#[test]
fn test_scope_with_multiple_params() {
    let scope = parse_scope("repo:*?action=create&action=delete");
    if let ParsedScope::Repo(repo) = scope {
        assert!(repo.actions.contains(&RepoAction::Create));
        assert!(repo.actions.contains(&RepoAction::Delete));
        assert!(!repo.actions.contains(&RepoAction::Update));
    } else {
        panic!("Expected Repo scope");
    }
}

#[test]
fn test_scope_invalid_action_rejects_whole_scope() {
    assert!(
        matches!(
            parse_scope("repo:*?action=invalid"),
            ParsedScope::Unknown(_)
        ),
        "an unrecognized action must not fall back to granting every action"
    );
    assert!(matches!(
        parse_scope("repo:*?action=create&action=invalid"),
        ParsedScope::Unknown(_)
    ));
}

#[test]
fn test_include_scope_parsing() {
    let scope = parse_scope("include:app.bsky.authFullApp?aud=did:web:api.bsky.app");
    if let ParsedScope::Include(inc) = scope {
        assert_eq!(inc.nsid, "app.bsky.authFullApp");
        assert_eq!(inc.aud, Some("did:web:api.bsky.app".to_string()));
    } else {
        panic!("Expected Include scope");
    }
}

#[test]
fn test_include_scope_no_aud() {
    let scope = parse_scope("include:com.example.authBasic");
    if let ParsedScope::Include(inc) = scope {
        assert_eq!(inc.nsid, "com.example.authBasic");
        assert!(inc.aud.is_none());
    } else {
        panic!("Expected Include scope");
    }
}

#[test]
fn test_identity_wildcard_vs_specific() {
    let wildcard = parse_scope("identity:*");
    let specific = parse_scope("identity:handle");

    assert!(matches!(wildcard, ParsedScope::Identity(i) if i.attr == IdentityAttr::Wildcard));
    assert!(matches!(specific, ParsedScope::Identity(i) if i.attr == IdentityAttr::Handle));
}

#[test]
fn test_identity_unknown_attr() {
    let scope = parse_scope("identity:unknown");
    assert!(matches!(scope, ParsedScope::Unknown(_)));
}

#[test]
fn test_transition_scopes_exact_match() {
    assert!(matches!(
        parse_scope("transition:generic"),
        ParsedScope::TransitionGeneric
    ));
    assert!(matches!(
        parse_scope("transition:chat.bsky"),
        ParsedScope::TransitionChat
    ));
    assert!(matches!(
        parse_scope("transition:email"),
        ParsedScope::TransitionEmail
    ));
    assert!(matches!(
        parse_scope("transition:unknown"),
        ParsedScope::Unknown(_)
    ));
}
