/* SPDX-License-Identifier: BSD-3-Clause */

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <setjmp.h>
#include <cmocka.h>

#include <tss2/tss2_mu.h>

#include "test_session_common.h"
#include "tpm2_session.h"

ESYS_TR _save_handle;

static void test_tpm2_create_dummy_context(TPMS_CONTEXT *context) {
    context->hierarchy = TPM2_RH_ENDORSEMENT;
    context->savedHandle = 2147483648;
    context->sequence = 10;
    context->contextBlob.size = 200;
    memset(context->contextBlob.buffer, '\0', context->contextBlob.size);
}

tool_rc __wrap_tpm2_context_save(ESYS_CONTEXT *esysContext, ESYS_TR saveHandle,
        TPMS_CONTEXT **context) {

    UNUSED(esysContext);

    // context should be non-null or bool files_save_tpm_context_to_file()
    // segfaults
    TPMS_CONTEXT *dummy_context = calloc(1, sizeof(TPMS_CONTEXT));
    test_tpm2_create_dummy_context(dummy_context);
    *context = dummy_context;
    _save_handle = saveHandle;

    return tool_rc_success;
}

TSS2_RC __wrap_Esys_ContextLoad(ESYS_CONTEXT *esysContext,
        const TPMS_CONTEXT *context, ESYS_TR *loadedHandle) {

    UNUSED(esysContext);
    UNUSED(context);

    *loadedHandle = _save_handle;

    return TPM2_RC_SUCCESS;
}

static TSS2_RC policy_restart_return() {
    return (TSS2_RC) mock();
}

TSS2_RC __wrap_Esys_PolicyRestart(ESYS_CONTEXT *esysContext,
        ESYS_TR sessionHandle, ESYS_TR shandle1, ESYS_TR shandle2,
        ESYS_TR shandle3) {

    UNUSED(esysContext);
    UNUSED(sessionHandle);
    UNUSED(shandle1);
    UNUSED(shandle2);
    UNUSED(shandle3);

    return policy_restart_return();
}

static ESYS_CONTEXT *CONTEXT = ((ESYS_CONTEXT *) 0xDEADBEEF);

TSS2_RC __wrap_Esys_TR_GetName(ESYS_CONTEXT *esysContext, ESYS_TR handle,
        TPM2B_NAME **name) {

    UNUSED(esysContext);
    UNUSED(handle);

    *name = malloc(sizeof(TPM2B_NAME));
    size_t offset = 0;
    TSS2_RC rc = Tss2_MU_TPM2_HANDLE_Marshal(SESSION_HANDLE, &(*name)->name[0],
            sizeof(TPM2_HANDLE), &offset);
    (*name)->size = offset;

    return rc;
}

TSS2_RC __wrap_tpm2_flush_context(ESYS_CONTEXT *esysContext,
        ESYS_TR flushHandle) {
    UNUSED(esysContext);
    UNUSED(flushHandle);

    return TSS2_RC_SUCCESS;
}

static void test_tpm2_session_defaults_good(void **state) {
    UNUSED(state);

    set_expected_defaults(TPM2_SE_POLICY, SESSION_HANDLE, TPM2_RC_SUCCESS);

    tpm2_session_data *d = tpm2_session_data_new(TPM2_SE_POLICY);
    assert_non_null(d);

    tpm2_session *s = NULL;
    tool_rc rc = tpm2_session_open(CONTEXT, d, &s);
    assert_int_equal(rc, tool_rc_success);
    assert_non_null(s);

    ESYS_TR handle = tpm2_session_get_handle(s);
    assert_int_equal(handle, SESSION_HANDLE);

    TPMI_ALG_HASH auth_hash = tpm2_session_get_authhash(s);
    assert_int_equal(auth_hash, TPM2_ALG_SHA256);

    tpm2_session_close(&s);
    assert_null(s);
}

static void test_tpm2_session_setters_good(void **state) {
    UNUSED(state);

    tpm2_session_data *d = tpm2_session_data_new(TPM2_SE_TRIAL);
    assert_non_null(d);

    tpm2_session_set_authhash(d, TPM2_ALG_SHA512);

    TPMT_SYM_DEF symmetric = {
        .algorithm = TPM2_ALG_AES,
        .keyBits = {
            .aes = 256
        },
        .mode = {
            .aes = 42
        },
    };

    tpm2_session_set_symmetric(d, &symmetric);

    tpm2_session_set_bind(d, 42);

    TPM2B_NONCE nonce = {
        .size = 5,
        .buffer = {
            'n', 'o', 'n', 'c', 'e'
        }
    };

    tpm2_session_set_nonce_caller(d, &nonce);

    tpm2_session_set_key(d, 0x1234);

    set_expected(0x1234, 42, TPM2_SE_TRIAL, &symmetric, TPM2_ALG_SHA512, &nonce,
    SESSION_HANDLE, TPM2_RC_SUCCESS);

    tpm2_session *s = NULL;
    tool_rc rc = tpm2_session_open(CONTEXT, d, &s);
    assert_int_equal(rc, tool_rc_success);
    assert_non_null(s);

    TPMI_SH_AUTH_SESSION handle = tpm2_session_get_handle(s);
    assert_int_equal(handle, SESSION_HANDLE);

    TPMI_ALG_HASH auth_hash = tpm2_session_get_authhash(s);
    assert_int_equal(auth_hash, TPM2_ALG_SHA512);

    tpm2_session_close(&s);
    assert_null(s);
}

static void test_tpm2_session_defaults_bad(void **state) {
    UNUSED(state);

    set_expected_defaults(TPM2_SE_POLICY, SESSION_HANDLE, TPM2_RC_FAILURE);

    tpm2_session_data *d = tpm2_session_data_new(TPM2_SE_POLICY);
    assert_non_null(d);

    tpm2_session *s = NULL;
    tool_rc rc = tpm2_session_open(CONTEXT, d, &s);
    assert_int_equal(rc, tool_rc_general_error);
    assert_null(s);
}

static int test_session_setup(void **state) {

    int rc = (*state = tmpnam(NULL)) == NULL;
    return rc;
}

static int test_session_teardown(void **state) {

    int rc = unlink((char *) *state);
    return rc;
}

static void test_tpm2_session_save(void **state) {

    set_expected_defaults(TPM2_SE_POLICY, SESSION_HANDLE, TPM2_RC_SUCCESS);

    tpm2_session_data *d = tpm2_session_data_new(TPM2_SE_POLICY);
    assert_non_null(d);

    tpm2_session_set_path(d, (char *) *state);

    tpm2_session *s = NULL;
    tool_rc rc = tpm2_session_open(CONTEXT, d, &s);
    assert_int_equal(rc, tool_rc_success);
    assert_non_null(s);

    ESYS_TR handle1 = tpm2_session_get_handle(s);
    TPMI_SH_AUTH_SESSION tpm_handle1;
    rc = tpm2_util_esys_handle_to_sys_handle(CONTEXT, handle1, &tpm_handle1);
    assert_int_equal(rc, tool_rc_success);

    rc = tpm2_session_close(&s);
    assert_int_equal(rc, tool_rc_success);
    assert_null(s);

    rc = tpm2_session_restore(NULL, (char *) *state, false, &s);
    assert_int_equal(rc, tool_rc_success);
    assert_non_null(s);

    ESYS_TR handle2 = tpm2_session_get_handle(s);
    TPMI_SH_AUTH_SESSION tpm_handle2;
    rc = tpm2_util_esys_handle_to_sys_handle(CONTEXT, handle2, &tpm_handle2);
    assert_int_equal(rc, tool_rc_success);

    assert_int_equal(tpm_handle1, tpm_handle2);

    tpm2_session_close(&s);
    assert_null(s);
}

static void test_tpm2_session_restart(void **state) {
    UNUSED(state);

    set_expected_defaults(TPM2_SE_POLICY, SESSION_HANDLE, TPM2_RC_SUCCESS);

    tpm2_session_data *d = tpm2_session_data_new(TPM2_SE_POLICY);
    assert_non_null(d);

    tpm2_session *s = NULL;
    tool_rc rc = tpm2_session_open(CONTEXT, d, &s);
    assert_int_equal(rc, tool_rc_success);
    assert_non_null(s);

    will_return(policy_restart_return, TPM2_RC_SUCCESS);
    rc = tpm2_session_restart(CONTEXT, s);
    assert_int_equal(rc, tool_rc_success);

    will_return(policy_restart_return, TPM2_RC_HANDLE);
    rc = tpm2_session_restart(CONTEXT, s);
    assert_int_equal(rc, tool_rc_general_error);

    tpm2_session_close(&s);
    assert_null(s);
}

static void test_tpm2_session_is_trial_test(void **state) {
    UNUSED(state);

    set_expected_defaults(TPM2_SE_TRIAL, SESSION_HANDLE, TPM2_RC_SUCCESS);

    tpm2_session_data *d = tpm2_session_data_new(TPM2_SE_TRIAL);
    assert_non_null(d);

    tpm2_session *s = NULL;
    tool_rc rc = tpm2_session_open(CONTEXT, d, &s);
    assert_int_equal(rc, tool_rc_success);
    assert_non_null(s);

    TPM2_SE type = tpm2_session_get_type(s);
    assert_int_equal(type, TPM2_SE_TRIAL);

    bool is_trial = tpm2_session_is_trial(s);
    assert_true(is_trial);

    tpm2_session_close(&s);
}

/* link required symbol, but tpm2_tool.c declares it AND main, which
 * we have a main below for cmocka tests.
 */
bool output_enabled = true;

int main(int argc, char *argv[]) {
    UNUSED(argc);
    UNUSED(argv);

    const struct CMUnitTest tests[] = {
    /*
     * no_init/bad_init routines must go first as there is no way to
     * de-initialize. However, re-initialization will query the capabilities
     * and can be changed or cause a no-match situation. This is a bit of
     * whitebox knowledge in the ordering of these tests.
     */
    cmocka_unit_test(test_tpm2_session_defaults_good),
    cmocka_unit_test(test_tpm2_session_setters_good),
    cmocka_unit_test(test_tpm2_session_defaults_bad),
    cmocka_unit_test_setup_teardown(test_tpm2_session_save,
            test_session_setup, test_session_teardown),
    cmocka_unit_test(test_tpm2_session_restart),
    cmocka_unit_test(test_tpm2_session_is_trial_test)
    };

    return cmocka_run_group_tests(tests, NULL, NULL);
}
