<?php
/**
 * Which hostnames this site owns — the derivation, and the one mistake that would
 * silently misattribute other people's traffic.
 *
 * Ownership used to be answered by a separate setting plus an invisible rule ("…and
 * anything under our own hostname"). It is one row now: `internal` under Medium
 * Mappings, whose values may carry a `*.` prefix and a `{this-domain}` token. Three
 * things read the result — the capture script, the shared-property snippet, and the
 * reprocess pass over stored journeys — so a mistake here reaches live capture AND
 * rewrites history.
 *
 * **THE MISTAKE THIS FILE EXISTS TO CATCH.** `{this-domain}` must expand to the site's
 * OWN hostname, never to the registrable domain above it. Sites are routinely siblings
 * under one parent: a hosting platform's demo domain, an agency's staging domain, a
 * company's product domains. Expand to the parent and `*.{this-domain}` quietly
 * declares every unrelated neighbour a property this site owns — their referrals stop
 * counting as acquisitions and are filed as "Own Property" instead, so a real source of
 * leads vanishes into a bucket labelled "us". Nothing errors. The totals stay
 * plausible. That is precisely why it needs a test and not a comment.
 *
 * Reads only. Nothing here writes an option; the settings arrays it exercises are built
 * in memory, and the stored option is hashed before and after to prove it.
 *
 * Usage:
 *   srv-gw wp --project <project> -- eval-file \
 *     wp-content/plugins/bw-lead-ai/tests/owned-hosts.php
 */

if ( ! defined( 'ABSPATH' ) ) {
	fwrite( STDERR, "Run via: wp eval-file\n" );
	exit( 1 );
}

if ( ! class_exists( 'BW_Lead_AI_Settings' ) ) {
	fwrite( STDERR, "BW Lead AI is not active on this site.\n" );
	exit( 1 );
}

$failures = array();

// A closure rather than a named function: wp-cli includes this file inside a method,
// so a named function could not reach $failures through `global`.
$check = function ( $desc, $got, $want ) use ( &$failures ) {
	$ok = ( maybe_serialize( $got ) === maybe_serialize( $want ) );
	if ( ! $ok ) {
		$failures[] = sprintf( '%s — expected %s, got %s', $desc, var_export( $want, true ), var_export( $got, true ) );
	}
	echo sprintf( "  %-4s %s\n", $ok ? 'PASS' : 'FAIL', $desc );
};

$before = md5( maybe_serialize( get_option( BW_LEAD_AI_OPTION, array() ) ) );

$own = BW_Lead_AI_Settings::own_hostname();
echo "Site hostname: {$own}\n\n";

if ( '' === $own || false === strpos( $own, '.' ) ) {
	fwrite( STDERR, "Cannot determine a dotted hostname for this site.\n" );
	exit( 1 );
}

// ---------------------------------------------------------------------------
echo "--- the parent-domain hazard ---\n";

// The domain one level up, and a fabricated neighbour under it. On a multi-tenant host
// that neighbour is a real, unrelated business.
$parent  = substr( $own, strpos( $own, '.' ) + 1 );
$sibling = 'bwlai-not-ours.' . $parent;

$check( '{this-domain} is the full hostname, not the parent', BW_Lead_AI_Settings::expand_host_value( '{this-domain}' ), $own );
$check( '{this-domain} does not resolve to ' . $parent, BW_Lead_AI_Settings::expand_host_value( '{this-domain}' ) === $parent, false );
$check( '*.{this-domain} expands under this site only', BW_Lead_AI_Settings::expand_host_value( '*.{this-domain}' ), '*.' . $own );

// The live rule, whatever this site has configured.
$owned = BW_Lead_AI_Settings::owned_hosts();
echo '  owned patterns: ' . ( empty( $owned ) ? '(none)' : implode( ', ', $owned ) ) . "\n";

$check( 'this site is its own', BW_Lead_AI_Settings::is_owned_host( $own, $owned ), true );
$check( 'a subdomain of this site is ours', BW_Lead_AI_Settings::is_owned_host( 'get.' . $own, $owned ), true );
$check( 'an unrelated neighbour is NOT ours', BW_Lead_AI_Settings::is_owned_host( $sibling, $owned ), false );
$check( 'nor is a subdomain of one', BW_Lead_AI_Settings::is_owned_host( 'www.' . $sibling, $owned ), false );
$check( 'nor is the parent domain itself', BW_Lead_AI_Settings::is_owned_host( $parent, $owned ), false );
// Not vacuous: the parent-expanded rule DOES claim the neighbour, which is the whole
// failure being guarded against.
$check( 'a parent-expanded rule WOULD claim it', BW_Lead_AI_Settings::host_matches( $sibling, '*.' . $parent ), true );

// ---------------------------------------------------------------------------
echo "\n--- matcher shapes ---\n";
$check( '*.example.com matches get.example.com', BW_Lead_AI_Settings::host_matches( 'get.example.com', '*.example.com' ), true );
$check( '*.example.com does NOT match example.com', BW_Lead_AI_Settings::host_matches( 'example.com', '*.example.com' ), false );
$check( 'example.com matches itself', BW_Lead_AI_Settings::host_matches( 'example.com', 'example.com' ), true );
$check( 'example.com matches get.example.com', BW_Lead_AI_Settings::host_matches( 'get.example.com', 'example.com' ), true );
$check( 'google matches www.google.co.uk', BW_Lead_AI_Settings::host_matches( 'www.google.co.uk', 'google' ), true );
$check( 'google does not match notgoogle.com', BW_Lead_AI_Settings::host_matches( 'notgoogle.com', 'google' ), false );

// ---------------------------------------------------------------------------
echo "\n--- the read-time migration ---\n";

$base = array( 'referrer_classification' => "organic : google, bing\nsocial : facebook", 'self_referral_hosts' => '' );

// A site configured before the row existed gets it, first, and it says exactly what
// the invisible rule used to say.
$text = BW_Lead_AI_Settings::referrer_classification_text( $base );
$check( 'no internal row → one is added first', strtok( $text, "\n" ), 'internal : *.{this-domain}' );
$check( 'and the rest is untouched', substr( $text, strpos( $text, "\n" ) + 1 ), $base['referrer_classification'] );

// Retired hostnames are folded into that row rather than lost.
$legacy = array_merge( $base, array( 'self_referral_hosts' => "get.example.com\nbook.example.net" ) );
$text   = BW_Lead_AI_Settings::referrer_classification_text( $legacy );
$check( 'retired hosts are folded into the row', strtok( $text, "\n" ), 'internal : *.{this-domain}, get.example.com, book.example.net' );
$check( 'and become owned', BW_Lead_AI_Settings::is_owned_host( 'get.example.com', BW_Lead_AI_Settings::owned_hosts( $legacy ) ), true );

// A hostname the row already covers is not added a second time in another shape.
$covered = array( 'referrer_classification' => "internal : *.example.com\norganic : google", 'self_referral_hosts' => 'get.example.com' );
$check( 'a host the row already covers is skipped', BW_Lead_AI_Settings::referrer_classification_text( $covered ), $covered['referrer_classification'] );

// One the row does NOT cover is appended, exactly as typed.
$partial = array( 'referrer_classification' => "internal : *.example.com\norganic : google", 'self_referral_hosts' => 'book.example.net' );
$check( 'an uncovered host is appended to the row', strtok( BW_Lead_AI_Settings::referrer_classification_text( $partial ), "\n" ), 'internal : *.example.com, book.example.net' );

// Idempotent: feeding the result back in changes nothing.
$once  = BW_Lead_AI_Settings::referrer_classification_text( $legacy );
$twice = BW_Lead_AI_Settings::referrer_classification_text( array_merge( $legacy, array( 'referrer_classification' => $once ) ) );
$check( 'running it twice changes nothing', $twice, $once );

// An empty internal row is a deliberate statement and is honoured.
$emptied = array( 'referrer_classification' => "internal :\norganic : google", 'self_referral_hosts' => '' );
$check( 'an empty internal row is left alone', BW_Lead_AI_Settings::referrer_classification_text( $emptied ), $emptied['referrer_classification'] );
$check( 'and yields no owned patterns', BW_Lead_AI_Settings::owned_hosts( $emptied ), array() );
$check( 'though this site is still its own', BW_Lead_AI_Settings::is_owned_host( $own, array() ), true );

// A fresh install and a migrated one must land in the same place — otherwise "what
// counts as mine" would quietly depend on when the site was set up.
$fresh = array( 'referrer_classification' => BW_Lead_AI_Settings::default_referrer_classification_text(), 'self_referral_hosts' => '' );
$check( 'the shipped default needs no migration', BW_Lead_AI_Settings::referrer_classification_text( $fresh ), $fresh['referrer_classification'] );
$check( 'a fresh install owns its own subdomains', BW_Lead_AI_Settings::owned_hosts( $fresh ), array( '*.' . $own ) );
$check( 'and a migrated one lands identically', BW_Lead_AI_Settings::owned_hosts( $base ), BW_Lead_AI_Settings::owned_hosts( $fresh ) );

// ---------------------------------------------------------------------------
echo "\n--- classification never sees the internal row ---\n";
$mediums = array();
foreach ( BW_Lead_AI_Settings::classification_rules( $legacy ) as $rule ) {
	$mediums[] = $rule['medium'];
}
$check( 'internal is not a classifier rule', in_array( 'internal', $mediums, true ), false );
$check( 'the ordinary rules survive', $mediums, array( 'organic', 'social' ) );

// ---------------------------------------------------------------------------
echo "\n--- nothing was written ---\n";
$check( 'the stored option is byte-identical', md5( maybe_serialize( get_option( BW_LEAD_AI_OPTION, array() ) ) ), $before );

echo "\n";
if ( empty( $failures ) ) {
	echo "PASS: owned-host derivation is correct and cannot claim a neighbouring site.\n";
	exit( 0 );
}
echo 'FAIL: ' . count( $failures ) . " problem(s):\n";
foreach ( $failures as $line ) {
	echo '  ' . $line . "\n";
}
exit( 1 );
