#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <cJSON.h>
#include <nostr_core/nip001.h>
#include <nostr_core/utils.h>

#include "../src/auth_envelope.h"

static int g_failures = 0;

static void check_condition(const char *name, int ok) {
    if (ok) {
        printf("[PASS] %s\n", name);
    } else {
        printf("[FAIL] %s\n", name);
        g_failures++;
    }
}

static int compute_params_hash_hex(cJSON *params, char out_hex[65]) {
    char *compact;
    unsigned char hash[32];

    if (params == NULL || out_hex == NULL) {
        return -1;
    }

    compact = cJSON_PrintUnformatted(params);
    if (compact == NULL) {
        return -1;
    }

    if (nostr_sha256((const unsigned char *)compact, strlen(compact), hash) != 0) {
        free(compact);
        return -1;
    }

    nostr_bytes_to_hex(hash, 32, out_hex);
    out_hex[64] = '\0';
    free(compact);
    return 0;
}

static cJSON *tag_pair(const char *k, const char *v) {
    cJSON *arr = cJSON_CreateArray();
    cJSON_AddItemToArray(arr, cJSON_CreateString(k));
    cJSON_AddItemToArray(arr, cJSON_CreateString(v));
    return arr;
}

static char *make_request_json(const unsigned char privkey[32],
                               const char *req_id,
                               const char *method,
                               int created_at) {
    cJSON *params = cJSON_CreateArray();
    cJSON *options = cJSON_CreateObject();
    cJSON *tags = cJSON_CreateArray();
    cJSON *auth;
    cJSON *root;
    char body_hash[65];
    char *out;

    cJSON_AddItemToArray(params, cJSON_CreateString("{\"kind\":1,\"content\":\"x\",\"tags\":[],\"created_at\":1700000000}"));
    cJSON_AddStringToObject(options, "role", "main");
    cJSON_AddItemToArray(params, options);

    if (compute_params_hash_hex(params, body_hash) != 0) {
        cJSON_Delete(params);
        return NULL;
    }

    cJSON_AddItemToArray(tags, tag_pair("nsigner_rpc", req_id));
    cJSON_AddItemToArray(tags, tag_pair("nsigner_method", method));
    cJSON_AddItemToArray(tags, tag_pair("nsigner_body_hash", body_hash));

    auth = nostr_create_and_sign_event(AUTH_EVENT_KIND, "test-client", tags, privkey, (time_t)created_at);
    if (auth == NULL) {
        cJSON_Delete(params);
        return NULL;
    }

    root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "id", req_id);
    cJSON_AddStringToObject(root, "method", method);
    cJSON_AddItemToObject(root, "params", params);
    cJSON_AddItemToObject(root, "auth", auth);

    out = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);
    return out;
}

int main(void) {
    auth_nonce_cache_t cache;
    unsigned char privkey[32] = {
        0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
    };
    char pubkey_hex[65];
    char label[64];
    const char *err_msg = NULL;
    int err_code = 0;
    char *req;

    check_condition("nostr_crypto_init", nostr_crypto_init() == 0);
    auth_nonce_cache_init(&cache);

    req = make_request_json(privkey, "req-1", "sign_event", (int)time(NULL));
    check_condition("build valid request", req != NULL);
    if (req != NULL) {
        check_condition("valid auth envelope accepted",
            auth_envelope_verify_request(req,
                                         &cache,
                                         AUTH_DEFAULT_SKEW_SECONDS,
                                         pubkey_hex,
                                         sizeof(pubkey_hex),
                                         label,
                                         sizeof(label),
                                         &err_code,
                                         &err_msg) == 0);

        check_condition("pubkey output length 64", strlen(pubkey_hex) == 64);
        free(req);
    }

    check_condition("missing auth rejected with required code",
        auth_envelope_verify_request("{\"id\":\"x\",\"method\":\"get_public_key\",\"params\":[]}",
                                     &cache,
                                     AUTH_DEFAULT_SKEW_SECONDS,
                                     pubkey_hex,
                                     sizeof(pubkey_hex),
                                     label,
                                     sizeof(label),
                                     &err_code,
                                     &err_msg) != 0 &&
        err_code == AUTH_ERR_ENVELOPE_REQUIRED);

    req = make_request_json(privkey, "req-2", "get_public_key", (int)time(NULL));
    check_condition("build second request", req != NULL);
    if (req != NULL) {
        cJSON *root = cJSON_Parse(req);
        cJSON *method_item;
        char *tampered;

        check_condition("parse second request", root != NULL);
        method_item = (root != NULL) ? cJSON_GetObjectItemCaseSensitive(root, "method") : NULL;
        if (method_item != NULL) {
            cJSON_SetValuestring(method_item, "sign_event");
        }
        tampered = (root != NULL) ? cJSON_PrintUnformatted(root) : NULL;
        cJSON_Delete(root);

        check_condition("tamper method mismatch rejected",
            tampered != NULL &&
            auth_envelope_verify_request(tampered,
                                         &cache,
                                         AUTH_DEFAULT_SKEW_SECONDS,
                                         pubkey_hex,
                                         sizeof(pubkey_hex),
                                         label,
                                         sizeof(label),
                                         &err_code,
                                         &err_msg) != 0 &&
            err_code == AUTH_ERR_ENVELOPE_MISMATCH);

        free(tampered);
        free(req);
    }

    req = make_request_json(privkey, "req-3", "sign_event", (int)time(NULL));
    check_condition("build replay request", req != NULL);
    if (req != NULL) {
        int first_ok = auth_envelope_verify_request(req,
                                                    &cache,
                                                    AUTH_DEFAULT_SKEW_SECONDS,
                                                    pubkey_hex,
                                                    sizeof(pubkey_hex),
                                                    label,
                                                    sizeof(label),
                                                    &err_code,
                                                    &err_msg);
        int second_ok = auth_envelope_verify_request(req,
                                                     &cache,
                                                     AUTH_DEFAULT_SKEW_SECONDS,
                                                     pubkey_hex,
                                                     sizeof(pubkey_hex),
                                                     label,
                                                     sizeof(label),
                                                     &err_code,
                                                     &err_msg);
        check_condition("first replay request accepted", first_ok == 0);
        check_condition("second replay request rejected", second_ok != 0 && err_code == AUTH_ERR_REPLAY_DETECTED);
        free(req);
    }

    req = make_request_json(privkey, "req-4", "sign_event", (int)time(NULL) - 1000);
    check_condition("build stale request", req != NULL);
    if (req != NULL) {
        check_condition("stale request rejected",
            auth_envelope_verify_request(req,
                                         &cache,
                                         AUTH_DEFAULT_SKEW_SECONDS,
                                         pubkey_hex,
                                         sizeof(pubkey_hex),
                                         label,
                                         sizeof(label),
                                         &err_code,
                                         &err_msg) != 0 &&
            err_code == AUTH_ERR_ENVELOPE_STALE);
        free(req);
    }

    nostr_crypto_cleanup();

    if (g_failures == 0) {
        printf("ALL TESTS PASSED\n");
        return 0;
    }

    printf("TESTS FAILED: %d\n", g_failures);
    return 1;
}
