/*
 * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.amplifyframework.api.aws.auth;

import androidx.annotation.NonNull;

import org.json.JSONException;
import org.json.JSONObject;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;

/**
 * Utility class to easily generate a fake JWT token with desired header and payload.
 * This class is not equipped to generate and append a valid signature to the token.
 */
public final class FakeJWTToken {
    private final String token;

    private FakeJWTToken(String token) {
        this.token = token;
    }

    /**
     * Get the generated JWT token string.
     * @return the generated token
     */
    public String asString() {
        return token;
    }

    /**
     * Get the builder instance for constructing a token.
     * @return the builder instance
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Builder instance to specify the contents of a generated JWT token.
     */
    public static final class Builder {
        private static final String TOKEN_SEPARATOR = ".";
        private static final String DEFAULT_SIGNATURE = "FAKE-SIGNATURE";
        private JSONObject header;
        private JSONObject payload;
        private String signature;

        private Builder() {
            this.header = new JSONObject();
            this.payload = new JSONObject();
            this.signature = DEFAULT_SIGNATURE;
        }

        /**
         * Sets the header and return self for chaining.
         * @param header the token header json
         * @return this builder instance
         */
        @NonNull
        public Builder setHeader(@NonNull JSONObject header) {
            this.header = Objects.requireNonNull(header);
            return this;
        }

        /**
         * Sets the payload and return self for chaining.
         * @param payload the token payload json
         * @return this builder instance
         */
        @NonNull
        public Builder setPayload(@NonNull JSONObject payload) {
            this.payload = Objects.requireNonNull(payload);
            return this;
        }

        /**
         * Put a key-value pair to the header and return self for chaining.
         * If the key already exists, then update the value.
         * @param key key to add to header
         * @param value value that corresponds with the key
         * @return this builder instance
         */
        @NonNull
        public Builder putHeader(@NonNull String key, @NonNull String value) {
            Objects.requireNonNull(key);
            Objects.requireNonNull(value);
            try {
                header.put(key, value);
            } catch (JSONException exception) {
                header.remove(key);
                return putHeader(key, value);
            }
            return this;
        }

        /**
         * Put a key-value pair to the payload and return self for chaining.
         * If the key already exists, then update the value.
         * @param key key to add to header
         * @param value value that corresponds with the key
         * @return this builder instance
         */
        @NonNull
        public Builder putPayload(@NonNull String key, @NonNull Object value) {
            Objects.requireNonNull(key);
            Objects.requireNonNull(value);
            try {
                payload.put(key, value);
            } catch (JSONException exception) {
                payload.remove(key);
                return putPayload(key, value);
            }
            return this;
        }

        /**
         * Sets the signature string and return self for chaining.
         * Defaults to "FAKE-SIGNATURE".
         * @param signature the signature for validation
         * @return this builder instance
         */
        @NonNull
        public Builder setSignature(@NonNull String signature) {
            this.signature = Objects.requireNonNull(signature);
            return this;
        }

        /**
         * Generate a fake JWT token with the contents of this builder.
         * @return a fake JWT token
         */
        @NonNull
        public FakeJWTToken build() {
            String token = base64Encode(header) +
                    TOKEN_SEPARATOR +
                    base64Encode(payload) +
                    TOKEN_SEPARATOR +
                    signature;
            return new FakeJWTToken(token);
        }

        private String base64Encode(JSONObject json) {
            byte[] bytes = json.toString().getBytes(StandardCharsets.UTF_8);
            return Base64.getEncoder().encodeToString(bytes);
        }
    }
}
