<?php
/**
 * Test-mode plumbing for "Test your widget".
 *
 * An admin-minted, short-lived token rides the front-end URL; the widget
 * reports its session id back with the token, and the session is flagged
 * is_test_call so it never appears in stats, leads, or notifications.
 *
 * @since      1.34.0
 *
 * @package    SiteStaffr
 * @subpackage SiteStaffr/includes
 */

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Mint/validate test tokens and flag widget sessions as test conversations.
 */
class SiteStaffr_Test_Mode {

	const TOKEN_TRANSIENT   = 'sitestaffr_test_widget_token';
	const PENDING_TRANSIENT = 'sitestaffr_pending_test_sessions';
	const ISSUED_TRANSIENT  = 'sitestaffr_test_widget_token_issued';
	const TOKEN_TTL         = 15 * MINUTE_IN_SECONDS;

	/**
	 * Front-end URL that opens the widget in test mode. Mints (or reuses)
	 * the current token. Only call from admin context.
	 *
	 * @return string
	 */
	public static function get_test_url() {
		$token = get_transient( self::TOKEN_TRANSIENT );
		if ( ! is_string( $token ) || '' === $token ) {
			$token = wp_generate_password( 24, false, false );
			set_transient( self::TOKEN_TRANSIENT, $token, self::TOKEN_TTL );
			// Remembered so the token can only flag conversations that happen AFTER it
			// was minted — see flag_session().
			set_transient( self::ISSUED_TRANSIENT, time(), self::TOKEN_TTL );
		}
		return add_query_arg( 'sitestaffr_test', rawurlencode( $token ), home_url( '/' ) );
	}

	/**
	 * When the current test token was minted (0 when unknown).
	 *
	 * @return int
	 */
	private static function token_issued_at() {
		return (int) get_transient( self::ISSUED_TRANSIENT );
	}

	/**
	 * Constant-time token check.
	 *
	 * @param string $token Candidate token.
	 * @return bool
	 */
	public static function validate_token( $token ) {
		$stored = get_transient( self::TOKEN_TRANSIENT );
		return is_string( $stored ) && '' !== $stored && hash_equals( $stored, (string) $token );
	}

	/**
	 * Flag a session as a test conversation. If the middleware hasn't
	 * created the call row yet, queue the session id so the insert
	 * handler flags it instead.
	 *
	 * @param string $session_id Session identifier.
	 */
	public static function flag_session( $session_id ) {
		global $wpdb;
		$calls_table = $wpdb->prefix . 'phoneease_calls';
		$existing    = $wpdb->get_row( $wpdb->prepare( 'SELECT id, created_at FROM %i WHERE session_id = %s', $calls_table, $session_id ) );
		if ( $existing ) {
			// A test token may only mark conversations that happened after it was
			// minted. Otherwise anyone holding a leaked test link could flag a REAL
			// conversation as a test, which silently drops it from stats and leads and
			// suppresses its notification email.
			$issued_at  = self::token_issued_at();
			$created_at = strtotime( (string) $existing->created_at . ' UTC' );
			if ( $issued_at > 0 && $created_at && $created_at < $issued_at ) {
				return;
			}
			SiteStaffr_Database::mark_as_test_call( $session_id );
			return;
		}
		$pending   = get_transient( self::PENDING_TRANSIENT );
		$pending   = is_array( $pending ) ? $pending : array();
		$pending[] = $session_id;
		set_transient( self::PENDING_TRANSIENT, array_slice( array_unique( $pending ), -20 ), HOUR_IN_SECONDS );
	}

	/**
	 * AJAX: flag a widget session as a test conversation (token-validated).
	 *
	 * The token is minted admin-side (get_test_url) and rides the front-end
	 * URL, so possession proves an admin opened the test link within the
	 * token TTL. Registered for both logged-in and nopriv contexts.
	 *
	 * @return void
	 */
	public static function ajax_flag_test_session() {
		$token      = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- the short-lived admin-minted token IS the credential.
		$session_id = isset( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing

		if ( '' === $session_id || '' === $token || ! self::validate_token( $token ) ) {
			wp_send_json_error( array( 'message' => 'Invalid or expired test token.' ), 403 );
		}

		self::flag_session( $session_id );
		wp_send_json_success();
	}

	/**
	 * Whether a session id was pre-flagged as test; consumes the entry.
	 *
	 * @param string $session_id Session identifier.
	 * @return bool
	 */
	public static function claim_pending( $session_id ) {
		$pending = get_transient( self::PENDING_TRANSIENT );
		if ( ! is_array( $pending ) || ! in_array( $session_id, $pending, true ) ) {
			return false;
		}
		set_transient( self::PENDING_TRANSIENT, array_values( array_diff( $pending, array( $session_id ) ) ), HOUR_IN_SECONDS );
		return true;
	}
}
