/*
 * test_libotppad.c — unit tests for libotppad.
 *
 * Covers: XOR, base64 round-trip, Padmé padding round-trip, ASCII armor
 * round-trip, binary header round-trip, .state file round-trip, and pad
 * checksum against a known pad.
 */
#define _POSIX_C_SOURCE 200809L

#include "libotppad.h"

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>

static int failures = 0;

#define CHECK(cond, msg) do { \
    if (!(cond)) { \
        fprintf(stderr, "FAIL: %s (%s:%d)\n", (msg), __FILE__, __LINE__); \
        failures++; \
    } else { \
        printf("ok: %s\n", (msg)); \
    } \
} while (0)

static void test_xor(void) {
    unsigned char data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
    unsigned char pad[]  = {0xff, 0xee, 0xdd, 0xcc, 0xbb};
    unsigned char out[5];
    CHECK(otppad_xor(data, 5, pad, out) == 0, "xor returns 0");
    CHECK(out[0] == 0xfe && out[1] == 0xec && out[2] == 0xde &&
          out[3] == 0xc8 && out[4] == 0xbe, "xor values correct");
    /* in-place aliasing */
    CHECK(otppad_xor(data, 5, pad, data) == 0, "xor in-place");
    CHECK(data[0] == 0xfe, "xor in-place value");
}

static void test_base64(void) {
    const char *in = "Hello, World!";
    int len = (int)strlen(in);
    char *enc = otppad_base64_encode((const unsigned char *)in, len);
    CHECK(enc != NULL, "base64 encode");
    int dlen = 0;
    unsigned char *dec = otppad_base64_decode(enc, &dlen);
    CHECK(dec != NULL, "base64 decode");
    CHECK(dlen == len, "base64 length preserved");
    CHECK(memcmp(dec, in, len) == 0, "base64 round-trip");
    free(enc);
    free(dec);

    /* empty input */
    enc = otppad_base64_encode((const unsigned char *)"", 0);
    CHECK(enc != NULL && strcmp(enc, "") == 0, "base64 empty");
    free(enc);
}

static void test_padding(void) {
    /* 10-byte message -> 256-byte bucket */
    size_t chunk = otppad_chunk_size(10);
    CHECK(chunk == 256, "chunk size for 10 bytes is 256");
    /* 300-byte message -> 512-byte bucket */
    chunk = otppad_chunk_size(300);
    CHECK(chunk == 512, "chunk size for 300 bytes is 512");
    /* 256-byte message -> 512 (needs +1 for 0x80) */
    chunk = otppad_chunk_size(256);
    CHECK(chunk == 512, "chunk size for 256 bytes is 512 (needs +1)");

    unsigned char buf[512];
    const char *msg = "test message";
    size_t msg_len = strlen(msg);
    memset(buf, 0xaa, sizeof(buf));
    memcpy(buf, msg, msg_len);
    chunk = otppad_chunk_size(msg_len);
    CHECK(otppad_pad_apply(buf, msg_len, chunk) == 0, "pad apply");
    CHECK(buf[msg_len] == 0x80, "pad marker at msg_len");
    size_t recovered;
    CHECK(otppad_pad_remove(buf, chunk, &recovered) == 0, "pad remove");
    CHECK(recovered == msg_len, "pad remove recovers length");
    CHECK(memcmp(buf, msg, msg_len) == 0, "pad round-trip preserves message");
}

static void test_armor(void) {
    const char *chksum =
        "333e9902db839d9d7f1f6aaa30f392a77c9abd011dd6274d9d3cf167361a789e";
    unsigned char ct[] = {0xde, 0xad, 0xbe, 0xef, 0x10, 0x20, 0x30, 0x40};
    uint64_t offset = 12345;
    char *armor = NULL;
    CHECK(otppad_armor_generate("v0.3.53", chksum, offset, ct, sizeof(ct),
                                &armor) == 0,
          "armor generate");
    CHECK(armor != NULL && strstr(armor, "BEGIN OTP MESSAGE") != NULL,
          "armor has begin marker");

    char parsed_chk[OTPPAD_CHKSUM_HEX_LEN + 1];
    uint64_t parsed_off;
    char parsed_b64[8192];
    CHECK(otppad_armor_parse(armor, parsed_chk, &parsed_off, parsed_b64,
                             sizeof(parsed_b64)) == 0,
          "armor parse");
    CHECK(strcmp(parsed_chk, chksum) == 0, "armor chksum round-trip");
    CHECK(parsed_off == offset, "armor offset round-trip");

    int dlen = 0;
    unsigned char *dec = otppad_base64_decode(parsed_b64, &dlen);
    CHECK(dec != NULL && dlen == (int)sizeof(ct), "armor b64 length");
    CHECK(dec && memcmp(dec, ct, sizeof(ct)) == 0, "armor ciphertext round-trip");
    free(armor);
    free(dec);
}

static void test_bin_header(void) {
    otppad_bin_header_t hdr;
    memset(&hdr, 0, sizeof(hdr));
    memcpy(hdr.magic, OTPPAD_MAGIC, OTPPAD_MAGIC_LEN);
    hdr.version = OTPPAD_FORMAT_VERSION;
    memset(hdr.pad_chksum, 0xab, OTPPAD_CHKSUM_BIN_LEN);
    hdr.pad_offset = 999;
    hdr.file_mode = 0644;
    hdr.file_size = 4096;

    const char *path = "test_bin_header.tmp";
    FILE *fp = fopen(path, "wb");
    CHECK(fp != NULL, "open bin header tmp for write");
    CHECK(otppad_bin_header_write(fp, &hdr) == 0, "bin header write");
    fclose(fp);

    fp = fopen(path, "rb");
    CHECK(fp != NULL, "open bin header tmp for read");
    otppad_bin_header_t hdr2;
    CHECK(otppad_bin_header_read(fp, &hdr2) == 0, "bin header read");
    fclose(fp);
    unlink(path);

    CHECK(hdr2.version == hdr.version, "bin header version");
    CHECK(hdr2.pad_offset == hdr.pad_offset, "bin header offset");
    CHECK(hdr2.file_mode == hdr.file_mode, "bin header file_mode");
    CHECK(hdr2.file_size == hdr.file_size, "bin header file_size");
    CHECK(memcmp(hdr2.pad_chksum, hdr.pad_chksum, OTPPAD_CHKSUM_BIN_LEN) == 0,
          "bin header chksum");
    CHECK(otppad_bin_is_magic((const unsigned char *)hdr2.magic, 4),
          "bin is_magic");
}

static void test_state(void) {
    const char *dir = "test_state_dir.tmp";
    mkdir(dir, 0755);
    const char *chksum =
        "333e9902db839d9d7f1f6aaa30f392a77c9abd011dd6274d9d3cf167361a789e";

    uint64_t off;
    CHECK(otppad_state_read(dir, chksum, &off) != 0, "state read missing fails");

    CHECK(otppad_state_write(dir, chksum, 42) == 0, "state write");
    CHECK(otppad_state_read(dir, chksum, &off) == 0, "state read");
    CHECK(off == 42, "state value");

    CHECK(otppad_state_write(dir, chksum, 1000) == 0, "state overwrite");
    CHECK(otppad_state_read(dir, chksum, &off) == 0, "state read 2");
    CHECK(off == 1000, "state value 2");

    /* cleanup */
    char path[1024];
    snprintf(path, sizeof(path), "%s/%s.state", dir, chksum);
    unlink(path);
    rmdir(dir);
}

static void test_checksum(void) {
    /* Build a tiny pad: 64 bytes, first 32 are the "key", rest arbitrary. */
    const char *dir = "test_chksum_dir.tmp";
    const char *chksum_expected =
        "333e9902db839d9d7f1f6aaa30f392a77c9abd011dd6274d9d3cf167361a789e";
    mkdir(dir, 0755);

    /* Use the real test pad if it exists; otherwise skip this test. */
    const char *real_pad =
        "/media/user/Music/pads/333e9902db839d9d7f1f6aaa30f392a77c9abd011dd6274d9d3cf167361a789e.pad";
    FILE *fp = fopen(real_pad, "rb");
    if (!fp) {
        printf("ok: checksum test skipped (no real pad at %s)\n", real_pad);
        rmdir(dir);
        return;
    }
    fclose(fp);

    char chk[OTPPAD_CHKSUM_HEX_LEN + 1];
    CHECK(otppad_checksum(real_pad, chk) == 0, "checksum computes");
    CHECK(strcmp(chk, chksum_expected) == 0,
          "checksum matches expected (bit-compatible with otp)");

    rmdir(dir);
}

int main(void) {
    test_xor();
    test_base64();
    test_padding();
    test_armor();
    test_bin_header();
    test_state();
    test_checksum();

    if (failures == 0) {
        printf("\nALL TESTS PASSED\n");
        return 0;
    }
    printf("\n%d TEST(S) FAILED\n", failures);
    return 1;
}
