/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License 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.
 */

/***************************************************************************
 * Copyright (C) 2017-2026 ZmartZone Holding BV
 * All rights reserved.
 *
 * DISCLAIMER OF WARRANTIES:
 *
 * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT
 * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING,
 * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  NOR ARE THERE ANY
 * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE
 * USAGE.  FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET
 * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE
 * WILL BE UNINTERRUPTED.  IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @Author: Hans Zandbelt - hans.zandbelt@openidc.com
 *
 **************************************************************************/

#include "util.h"
#include "cfg/cfg_int.h"
#include "cfg/dir.h"
#include "handle/handle.h"
#include "metadata.h"
#include "proto/proto.h"
#include "session.h"
#include "util/util.h"
#include <openssl/evp.h>

/*
 * Per-test fixture state. oidc_test_setup() recreates both every test and
 * oidc_test_teardown() frees and NULLs them, so they do not carry over between
 * tests even under CK_FORK=no (make valgrind), where the whole suite runs in
 * one process. Tests that keep their own module-level statics (e.g. the cache
 * mocks in test_cache.c) must reset them in their fixture for the same reason.
 */
static apr_pool_t *pool = NULL;
static request_rec *request = NULL;

/*
 * memoize the real PBKDF2-HMAC-SHA256 derivation (210,000 iterations, see
 * oidc_util_key_derive_passphrase_key) by raw secret text, for the lifetime of the test
 * process. oidc_test_setup() re-derives on every one of the ~570 tests in the suite, and
 * test_cache.c's passphrase-rotation tests re-derive several times per test, but the whole
 * suite only ever uses a handful of distinct secret strings -- so caching collapses ~600 KDF
 * calls down to a handful without weakening what any test actually checks (the KDF itself is
 * only under test in test_util.c's test_util_jwt, which calls oidc_crypto_passphrase_derive_keys()
 * directly and bypasses this cache). This matters most under `make valgrind` (CK_FORK=no),
 * where the entire suite runs in one process and memcheck instrumentation makes each 210,000-
 * iteration call ~100x slower (~20ms natively vs. ~2s under valgrind).
 */
#define OIDC_TEST_KDF_CACHE_MAX 32
static struct {
	char secret[128];
	unsigned char key[OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN];
} oidc_test_kdf_cache[OIDC_TEST_KDF_CACHE_MAX];
static int oidc_test_kdf_cache_n = 0;

static apr_byte_t oidc_test_key_derive_cached(const char *secret, unsigned char *out) {
	int i;
	for (i = 0; i < oidc_test_kdf_cache_n; i++) {
		if (_oidc_strcmp(oidc_test_kdf_cache[i].secret, secret) == 0) {
			_oidc_memcpy(out, oidc_test_kdf_cache[i].key, OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN);
			return TRUE;
		}
	}
	if (oidc_util_key_derive_passphrase_key(secret, out, OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN) == FALSE)
		return FALSE;
	if ((oidc_test_kdf_cache_n < OIDC_TEST_KDF_CACHE_MAX) &&
	    (_oidc_strlen(secret) < sizeof(oidc_test_kdf_cache[0].secret))) {
		_oidc_strcpy(oidc_test_kdf_cache[oidc_test_kdf_cache_n].secret, secret);
		_oidc_memcpy(oidc_test_kdf_cache[oidc_test_kdf_cache_n].key, out,
			     OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN);
		oidc_test_kdf_cache_n++;
	}
	return TRUE;
}

/*
 * test-only drop-in for oidc_cfg_crypto_passphrase_derive_keys()/oidc_crypto_passphrase_derive_keys()
 * that routes the actual KDF work through the cache above; same semantics (skips a slot whose
 * *_set flag is already TRUE, treats an empty secret as "not configured").
 */
static apr_byte_t oidc_test_crypto_passphrase_derive_keys_cached(oidc_crypto_passphrase_t *cp) {
	if ((cp->secret1 != NULL) && (_oidc_strlen(cp->secret1) > 0) && (cp->derived_key1_set == FALSE)) {
		if (oidc_test_key_derive_cached(cp->secret1, cp->derived_key1) == FALSE)
			return FALSE;
		cp->derived_key1_set = TRUE;
	}
	if ((cp->secret2 != NULL) && (_oidc_strlen(cp->secret2) > 0) && (cp->derived_key2_set == FALSE)) {
		if (oidc_test_key_derive_cached(cp->secret2, cp->derived_key2) == FALSE)
			return FALSE;
		cp->derived_key2_set = TRUE;
	}
	return TRUE;
}

static request_rec *oidc_test_request_init(apr_pool_t *pool) {
	const unsigned int kIdx = 0;
	const unsigned int kEls = kIdx + 1;
	request_rec *request = (request_rec *)apr_pcalloc(pool, sizeof(request_rec));

	apr_pool_create(&request->pool, pool);

	request->subprocess_env = apr_table_make(request->pool, 0);

	request->headers_in = apr_table_make(request->pool, 0);
	request->headers_out = apr_table_make(request->pool, 0);
	request->err_headers_out = apr_table_make(request->pool, 0);

	apr_table_set(request->headers_in, "Host", "www.example.com");
	apr_table_set(request->headers_in, "OIDC_foo", "some-value");
	apr_table_set(request->headers_in, "Cookie",
		      "foo=bar; "
		      "mod_auth_openidc_session"
		      "=0123456789abcdef; baz=zot");

	request->server = apr_pcalloc(pool, sizeof(struct server_rec));
	request->server->process = apr_pcalloc(pool, sizeof(struct process_rec));
	apr_pool_create(&request->server->process->pool, pool);
	apr_pool_create(&request->server->process->pconf, pool);
	request->connection = apr_pcalloc(pool, sizeof(struct conn_rec));
	request->connection->bucket_alloc = apr_bucket_alloc_create(pool);
	request->connection->local_addr = apr_pcalloc(pool, sizeof(apr_sockaddr_t));
	/* minimal output filter carrying the request so the ap_pass_brigade stub
	 * can capture sent response bodies into the "sent_body" request state */
	request->output_filters = apr_pcalloc(pool, sizeof(ap_filter_t));
	request->output_filters->r = request;

	apr_pool_userdata_set("https", "scheme", NULL, request->pool);
	request->server->server_hostname = "www.example.com";
	request->connection->local_addr->port = 4433;
	request->unparsed_uri = "/bla?foo=bar&param1=value1";
	request->args = "foo=bar&param1=value1";
	apr_uri_parse(request->pool, "https://www.example.com/bla?foo=bar&param1=value1", &request->parsed_uri);

	auth_openidc_module.module_index = kIdx;
	oidc_cfg_t *cfg = oidc_cfg_server_create(request->server->process->pconf, request->server);

	oidc_cfg_provider_issuer_set(request->server->process->pconf, oidc_cfg_provider_get(cfg),
				     "https://idp.example.com");
	oidc_cfg_provider_authorization_endpoint_url_set(request->server->process->pconf, oidc_cfg_provider_get(cfg),
							 "https://idp.example.com/authorize");
	oidc_cfg_provider_client_id_set(request->server->process->pconf, oidc_cfg_provider_get(cfg), "client_id");

	cfg->redirect_uri = "https://www.example.com/protected/";

	oidc_dir_cfg_t *d_cfg = oidc_cfg_dir_config_create(request->server->process->pconf, NULL);

	// coverity[suspicious_sizeof]
	request->server->module_config = apr_pcalloc(request->server->process->pconf, sizeof(void *) * kEls);
	// coverity[suspicious_sizeof]
	request->per_dir_config = apr_pcalloc(request->server->process->pconf, sizeof(void *) * kEls);
	ap_set_module_config(request->server->module_config, &auth_openidc_module, cfg);
	ap_set_module_config(request->per_dir_config, &auth_openidc_module, d_cfg);

	// TODO:
	cfg->public_keys = apr_array_make(request->server->process->pconf, 1, sizeof(const char *));
	cfg->private_keys = apr_array_make(request->server->process->pconf, 1, sizeof(const char *));

	cfg->crypto_passphrase.secret1 = "12345678901234567890123456789012";
	if (oidc_test_crypto_passphrase_derive_keys_cached(&cfg->crypto_passphrase) == FALSE) {
		fprintf(stderr, "oidc_cfg_crypto_passphrase_derive_keys failed!\n");
		exit(-1);
	}
	cfg->cache.impl = &oidc_cache_shm;
	cfg->cache.cfg = NULL;
	cfg->cache.shm_size_max = 500;
	cfg->cache.shm_entry_size_max = 16384 + 255 + 17;
	cfg->cache.encrypt = 1;
	/* full post-config so the cache backend AND the shared refresh-grant mutex get set up */
	if (oidc_cfg_post_config(request->server->process->pconf, cfg, request->server) != OK) {
		fprintf(stderr, "oidc_cfg_post_config failed!\n");
		exit(-1);
	}

	if (oidc_cfg_dir_post_config(request->server) != OK) {
		fprintf(stderr, "oidc_util_jwt_post_config failed!\n");
		exit(-1);
	}

	if (oidc_util_jwt_post_config(request->server) != OK) {
		fprintf(stderr, "oidc_util_jwt_post_config failed!\n");
		exit(-1);
	}

	oidc_http_curl_pool_init(request->server->process->pconf);

	return request;
}

void oidc_test_setup(void) {
	apr_initialize();
	oidc_pre_config_init();
	/* reset the stubbed AuthType so a test that changed it does not leak into
	 * the next one under CK_FORK=no */
	oidc_test_set_auth_type(NULL);
	apr_pool_create(&pool, NULL);
	request = oidc_test_request_init(pool);
}

void oidc_test_teardown(void) {
	/* release the process-wide refresh mutex before apr_terminate frees its pool */
	if (request != NULL) {
		oidc_cfg_t *cfg = oidc_test_cfg_get();
		oidc_cfg_process_cleanup(cfg, request->server);
	}
	EVP_cleanup();
	apr_terminate();
	request = NULL;
	pool = NULL;
}

apr_pool_t *oidc_test_pool_get(void) {
	return pool;
}

request_rec *oidc_test_request_get(void) {
	return request;
}

oidc_cfg_t *oidc_test_cfg_get(void) {
	return (oidc_cfg_t *)ap_get_module_config(request->server->module_config, &auth_openidc_module);
}

cmd_parms *oidc_test_cmd_get(const char *primitive) {
	request_rec *r = oidc_test_request_get();
	cmd_parms *cmd = apr_pcalloc(r->pool, sizeof(cmd_parms));
	cmd->server = r->server;
	cmd->pool = r->pool;
	cmd->temp_pool = r->pool;
	cmd->directive = apr_pcalloc(cmd->pool, sizeof(ap_directive_t));
	cmd->directive->directive = primitive;
	return cmd;
}

/*
 * tests that poke cfg->crypto_passphrase.secret{1,2} directly at runtime (to simulate a
 * passphrase rotation without spinning up a new config) must call this afterwards so the
 * cached PBKDF2-derived key material (see oidc_cfg_crypto_passphrase_derive_keys()) is
 * recomputed for the new raw secret(s); in production this only ever happens once, at
 * post_config time, after a config reload
 */
void oidc_test_crypto_passphrase_rederive(oidc_cfg_t *cfg) {
	cfg->crypto_passphrase.derived_key1_set = FALSE;
	cfg->crypto_passphrase.derived_key2_set = FALSE;
	if (oidc_test_crypto_passphrase_derive_keys_cached(&cfg->crypto_passphrase) == FALSE) {
		fprintf(stderr, "oidc_cfg_crypto_passphrase_derive_keys failed!\n");
		exit(-1);
	}
}
