package cloud.nstart.terravault.ordinis.auth;

import cloud.nstart.terravault.ordinis.domain.DataScope;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

class ScopeContextTest {

  private final OrdinisAuthProperties propsWithQuery =
      new OrdinisAuthProperties(null, null, null, "realm_access.roles", false, true);
  private final OrdinisAuthProperties propsNoQuery =
      new OrdinisAuthProperties(null, null, null, "realm_access.roles", false, false);

  @AfterEach
  void clearContext() {
    SecurityContextHolder.clearContext();
  }

  // ============= role → scope mapping =============

  @Test
  void mapsOrdinisDashRolesToScopes() {
    setJwtWithRealmRoles(List.of("ordinis-public", "ordinis-internal"));
    assertThat(new ScopeContext(propsNoQuery).currentScopes())
        .containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL);
  }

  @Test
  void mapsUnderscoreUppercaseRolesToScopes() {
    setJwtWithRealmRoles(List.of("ORDINIS_RESTRICTED"));
    assertThat(new ScopeContext(propsNoQuery).currentScopes())
        .containsExactly(DataScope.RESTRICTED);
  }

  @Test
  void mapsBareScopeNamesToScopes() {
    setJwtWithRealmRoles(List.of("PUBLIC", "INTERNAL", "RESTRICTED"));
    assertThat(new ScopeContext(propsNoQuery).currentScopes())
        .containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL, DataScope.RESTRICTED);
  }

  @Test
  void mapsArbitrarySuffixedRoles() {
    setJwtWithRealmRoles(List.of("any-prefix-PUBLIC", "x_INTERNAL"));
    assertThat(new ScopeContext(propsNoQuery).currentScopes())
        .containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL);
  }

  @Test
  void mapsAltumColonSeparatedRoles() {
    // Альтум-style: realm role "ordinis:client:public" → DataScope.PUBLIC.
    setJwtWithRealmRoles(List.of(
        "ordinis:client:public",
        "ordinis:client:internal",
        "ordinis:client:restricted"));
    assertThat(new ScopeContext(propsNoQuery).currentScopes())
        .containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.INTERNAL, DataScope.RESTRICTED);
  }

  @Test
  void mapsMixedColonAndDashRoles() {
    setJwtWithRealmRoles(List.of("ordinis:client:public", "ordinis-restricted"));
    assertThat(new ScopeContext(propsNoQuery).currentScopes())
        .containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.RESTRICTED);
  }

  @Test
  void unrecognizedRolesGivePublicFallback() {
    setJwtWithRealmRoles(List.of("admin", "viewer"));
    assertThat(new ScopeContext(propsNoQuery).currentScopes())
        .containsExactly(DataScope.PUBLIC);
  }

  // ============= JWT vs query =============

  @Test
  void anonymousNoQueryReturnsPublic() {
    setAnonymous();
    assertThat(new ScopeContext(propsNoQuery).resolveForRead(null))
        .containsExactly(DataScope.PUBLIC);
  }

  @Test
  void anonymousQueryAllowedReturnsRequestedScopes() {
    setAnonymous();
    assertThat(new ScopeContext(propsWithQuery).resolveForRead("internal,restricted"))
        .containsExactlyInAnyOrder(DataScope.INTERNAL, DataScope.RESTRICTED);
  }

  @Test
  void anonymousQueryDisallowedAlwaysReturnsPublic() {
    setAnonymous();
    assertThat(new ScopeContext(propsNoQuery).resolveForRead("internal,restricted"))
        .containsExactly(DataScope.PUBLIC);
  }

  @Test
  void jwtAlwaysWinsOverQueryParam() {
    setJwtWithRealmRoles(List.of("ordinis-public"));
    Set<DataScope> scopes = new ScopeContext(propsWithQuery).resolveForRead("RESTRICTED");
    assertThat(scopes).containsExactly(DataScope.PUBLIC);
  }

  // ============= claim path resolution =============

  @Test
  void supportsCustomClaimPath() {
    var props = new OrdinisAuthProperties(null, null, null, "scopes", false, false);
    setJwtWithFlatScopes(List.of("PUBLIC", "RESTRICTED"));
    assertThat(new ScopeContext(props).currentScopes())
        .containsExactlyInAnyOrder(DataScope.PUBLIC, DataScope.RESTRICTED);
  }

  @Test
  void emptyClaimReturnsPublic() {
    setJwtWithRealmRoles(List.of());
    assertThat(new ScopeContext(propsNoQuery).currentScopes())
        .containsExactly(DataScope.PUBLIC);
  }

  // ============= helpers =============

  private void setJwtWithRealmRoles(List<String> roles) {
    Jwt jwt = Jwt.withTokenValue("dummy")
        .header("alg", "RS256")
        .issuedAt(Instant.now())
        .expiresAt(Instant.now().plusSeconds(3600))
        .claim("realm_access", Map.of("roles", roles))
        .build();
    SecurityContextHolder.getContext()
        .setAuthentication(new JwtAuthenticationToken(jwt, AuthorityUtils.NO_AUTHORITIES));
  }

  private void setJwtWithFlatScopes(List<String> scopes) {
    Jwt jwt = Jwt.withTokenValue("dummy")
        .header("alg", "RS256")
        .issuedAt(Instant.now())
        .expiresAt(Instant.now().plusSeconds(3600))
        .claim("scopes", scopes)
        .build();
    SecurityContextHolder.getContext()
        .setAuthentication(new JwtAuthenticationToken(jwt, AuthorityUtils.NO_AUTHORITIES));
  }

  private void setAnonymous() {
    SecurityContextHolder.getContext().setAuthentication(
        new AnonymousAuthenticationToken("anon", "anon",
            AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")));
  }
}
