/*
 * Copyright (c) 2024. Das Land Schleswig-Holstein vertreten durch das Ministerium für Energiewende, Klimaschutz, Umwelt und Natur
 * Zentrales IT-Management
 *
 * Lizenziert unter der EUPL, Version 1.2 oder - sobald
 * diese von der Europäischen Kommission genehmigt wurden -
 * Folgeversionen der EUPL ("Lizenz");
 * Sie dürfen dieses Werk ausschließlich gemäß
 * dieser Lizenz nutzen.
 * Eine Kopie der Lizenz finden Sie hier:
 *
 * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
 *
 * Sofern nicht durch anwendbare Rechtsvorschriften
 * gefordert oder in schriftlicher Form vereinbart, wird
 * die unter der Lizenz verbreitete Software "so wie sie
 * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
 * ausdrücklich oder stillschweigend - verbreitet.
 * Die sprachspezifischen Genehmigungen und Beschränkungen
 * unter der Lizenz sind dem Lizenztext zu entnehmen.
 */
package de.ozgcloud.admin.security;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.test.context.support.WithSecurityContext;
import org.springframework.security.test.context.support.WithSecurityContextFactory;
import org.springframework.util.StringUtils;

import com.nimbusds.jwt.JWTClaimNames;

import lombok.RequiredArgsConstructor;
import net.minidev.json.JSONObject;
import net.minidev.json.parser.JSONParser;
import net.minidev.json.parser.ParseException;

/**
 * Annotation to setup test {@link SecurityContext} with an {@link Authentication}. Adjusted from source:
 * com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt Author: Jérôme Wacongne <ch4mp@c4-soft.com>
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(factory = WithJwt.AuthenticationFactory.class)
public @interface WithJwt {

	String value() default "";

	String bearerString() default AuthenticationFactory.DEFAULT_BEARER;

	String headers() default AuthenticationFactory.DEFAULT_HEADERS;

	@RequiredArgsConstructor
	final class AuthenticationFactory implements WithSecurityContextFactory<WithJwt> {
		static final String DEFAULT_BEARER = "test.jwt.bearer";
		static final String DEFAULT_HEADERS = "{\"alg\": \"none\"}";

		private final Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter;

		@Override
		public SecurityContext createSecurityContext(WithJwt annotation) {
			var auth = authentication(annotation);

			var securityContext = SecurityContextHolder.createEmptyContext();
			securityContext.setAuthentication(auth);

			return securityContext;
		}

		private AbstractAuthenticationToken authentication(WithJwt annotation) {
			var claims = parseJson(annotation.value());
			var headers = parseJson(annotation.headers());
			var bearerString = annotation.bearerString();

			var now = Instant.now();
			var iat = Optional.ofNullable((Integer) claims.get(JWTClaimNames.ISSUED_AT)).map(Instant::ofEpochSecond).orElse(now);
			var exp = Optional.ofNullable((Integer) claims.get(JWTClaimNames.EXPIRATION_TIME)).map(Instant::ofEpochSecond)
					.orElse(now.plusSeconds(42));

			var jwt = new Jwt(bearerString, iat, exp, headers, claims);

			return jwtAuthenticationConverter.convert(jwt);
		}

		private static Map<String, Object> parseJson(String json) {
			if (!StringUtils.hasText(json)) {
				return Map.of();
			}
			try {
				return new JSONParser(JSONParser.MODE_PERMISSIVE).parse(json, JSONObject.class);
			} catch (final ParseException e) {
				throw new RuntimeException("Invalid JSON payload in @WithJwt");
			}
		}
	}
}