<?php

namespace Tests\Feature;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

use App\Models\{Address,Domain,Setup,System};

/**
 * Here we test mail routing.
 *
 * - COMMON to all routing nodes
 *   + We have RCs defined - everything for a node in the Region goes to an RC, unless RC's node defined locally, or nodes assigned to ZC
 *     ie: region_id is the same, find the NC/HUB/NODE address for the system_id that has been assigned 0/0 in the same region.
 *         nodes assigned to ZC have region_id = 0. Nodes with hub_id = NULL and host_id=0 are RC nodes.
 *   + We have NCs defined - everything under the Host goes to the NC, unless NC's nodes defined locally or nodes assigned to the RC
 *     ie: node_id defines the NCs responsible. Nodes with hub_id = NULL are an NC node.
 *   + We have HUBs defined - everything under the Hub goes to the HUB, unless HUB's nodes defined locally
 *     ie: hub_id defines who the hub is.
 *   + We have NODES defined - mail collected locally, including mail for it's point
 *
 * - Routing scenarios:
 *   - We are ZC for a domain
 *     + See COMMON
 *     + Everything else is collected locally
 *
 *   - We are RC for a domain
 *     + See COMMON
 *     + Any nodes in our RC not defined with an NC is collected locally, including points
 *     + Everything else is sent to the ZC (our uplink)
 *
 *   - We are NC for a domain
 *     + See COMMON
 *     + Any nodes in our NC not defined with a HUB is collected locally, including points
 *     + Everything else is sent to the RC or ZC (our uplink)
 *
 *   - We are a Hub for a domain
 *     + See COMMON
 *     + Any nodes in our HUB is collected locally, including points
 *     + Everything else is sent to the NC or RC (our uplink)
 *
 *   - We are a Node in a domain
 *     + See COMMON
 *     + Everything else is sent to the HUB or Host or RC (our uplink)
 *
 * @see Address::uplink()
 * @see Address::downlinks()
 */
class RoutingTest extends TestCase
{
	use DatabaseTransactions;

	public const ZONE_ADDRS = 4061;
	public const REGION_ADDRS = 811;
	public const NET_ADDRS = 161;
	public const HUB_ADDRS = 31;
	public const NODE_ADDRS = 6;

	private function zone(): Collection
	{
		$do = Domain::where('name','a')->sole();
		$zo = $do->zones->where('zone_id',100)->pop();

		return $zo->addresses;
	}

	private function session_zc(): void
	{
		$ao = Address::findFTN('101:0/0@a');
		$ao->system->sessions()->attach([$ao->zone_id=>['sespass'=>'ABCD']]);
	}

	private function session_rc(): void
	{
		$ao = Address::findFTN('101:1/0@a');
		$ao->system->sessions()->attach([$ao->zone_id=>['sespass'=>'ABCD']]);
	}

	private function session_nc(): void
	{
		$ao = Address::findFTN('101:10/0@a');
		$ao->system->sessions()->attach([$ao->zone_id=>['sespass'=>'ABCD']]);
	}

	private function session_hub(): void
	{
		$ao = Address::findFTN('101:10/100@a');
		$ao->system->sessions()->attach([$ao->zone_id=>['sespass'=>'ABCD']]);
	}

	private function session_node(): void
	{
		$ao = Address::findFTN('101:10/101@a');
		$ao->system->sessions()->attach([$ao->zone_id=>['sespass'=>'ABCD']]);
	}

	private function session_point(): void
	{
		$ao = Address::findFTN('101:10/101.1@a');
		$ao->system->sessions()->attach([$ao->zone_id=>['sespass'=>'ABCD']]);
	}

	// Make sure I'm configured correctly for the rest of the tests
	public function test_my_addresses()
	{
		$ftns = our_address();
		$this->assertEquals(6,$ftns->count());
	}

	// Get a list of active addresses in a Zone
	public function test_zone_addresses()
	{
		$nodes = $this->zone();
		$this->assertEquals(self::ZONE_ADDRS,$nodes->count());
	}

	// Get a ZC with no session details, and make sure it has no parent nor children
	public function test_zc_no_session()
	{
		// Pick ZC without any session info - we have 0 children
		$ao = Address::findFTN('101:0/0@a');

		$this->assertEquals($ao->role_id,Address::NODE_ZC);
		$this->assertCount(0,$ao->downlinks());
		$this->assertNull($ao->uplink());
	}

	// If we have a ZC, make sure we are routing to all it's children
	public function test_zc_session_children()
	{
		$ao = Address::findFTN('101:0/0@a');

		// This is a ZC
		$this->assertEquals($ao->role_name,'ZC');
		$this->assertEquals(Address::NODE_ZC,$ao->role_id);

		// Children
		$this->assertCount(self::ZONE_ADDRS-1,$ao->children());
		$this->assertCount(0,$ao->downlinks());

		$this->session_zc();
		$ao->refresh();

		// Children
		$this->assertCount(self::ZONE_ADDRS,$ao->downlinks());
	}

	// ZC uplink should be null, and the parent should be itself
	public function test_zc_parent()
	{
		$ao = Address::findFTN('101:0/0@a');

		// This uplink should also be null
		$this->assertNull($ao->uplink()?->ftn);

		$this->session_zc();
		$ao->refresh();

		// This parent should also be null
		$this->assertNull($ao->parent()?->ftn);

		// This uplink should be itself
		$this->assertEquals($ao->ftn,$ao->uplink()?->ftn);
	}

	public function test_rc_session_children()
	{
		$ao = Address::findFTN('101:1/0@a');

		// This is a RC
		$this->assertEquals($ao->role_name,'RC');
		$this->assertEquals(Address::NODE_RC,$ao->role_id);

		// Children
		$this->assertCount(self::REGION_ADDRS-1,$ao->children());
		$this->assertCount(0,$ao->downlinks());

		$this->session_rc();
		$ao->refresh();

		// This is a ZC
		$this->assertCount(self::REGION_ADDRS,$ao->downlinks());
	}

	public function test_rc_parent()
	{
		$ao = Address::findFTN('101:1/0@a');
		$o = Address::findFTN('101:0/0@a');

		// This uplink should also be null
		$this->assertNull($ao->uplink()?->ftn);

		$this->session_rc();
		$ao->refresh();

		// This parent should also be null
		$this->assertEquals($o->ftn,$ao->parent()?->ftn);

		// This uplink should be itself
		$this->assertEquals($ao->ftn,$ao->uplink()?->ftn);
	}

	public function test_zc_rc_nodes()
	{
		$this->session_zc();
		$this->session_rc();

		$ao = Address::findFTN('101:0/0@a');
		$this->assertCount(self::ZONE_ADDRS-self::REGION_ADDRS,$ao->downlinks());

		$ao = Address::findFTN('101:1/0@a');
		$this->assertCount(self::REGION_ADDRS,$ao->downlinks());
	}

	public function test_nc_session_children()
	{
		$ao = Address::findFTN('101:10/0@a');

		// This is a NC
		$this->assertEquals($ao->role_name,'NC');
		$this->assertEquals(Address::NODE_NC,$ao->role_id);

		// Children
		$this->assertCount(self::NET_ADDRS-1,$ao->children());
		$this->assertCount(0,$ao->downlinks());

		$this->session_nc();
		$ao->refresh();

		// This is a NC
		$this->assertCount(self::NET_ADDRS,$ao->downlinks());
	}

	public function test_nc_parent()
	{
		$ao = Address::findFTN('101:10/0@a');
		$o = Address::findFTN('101:1/0@a');

		// This uplink should also be null
		$this->assertNull($ao->uplink()?->ftn);

		$this->session_nc();
		$ao->refresh();

		// This parent should also be null
		$this->assertEquals($o->ftn,$ao->parent()?->ftn);

		// This uplink should be itself
		$this->assertEquals($ao->ftn,$ao->uplink()?->ftn);
	}

	public function test_zc_rc_nc_nodes()
	{
		$this->session_zc();
		$this->session_rc();
		$this->session_nc();

		$ao = Address::findFTN('101:0/0@a');
		$this->assertCount(self::ZONE_ADDRS-self::REGION_ADDRS,$ao->downlinks());

		$ao = Address::findFTN('101:1/0@a');
		$this->assertCount(self::REGION_ADDRS-self::NET_ADDRS,$ao->downlinks());

		$ao = Address::findFTN('101:10/0@a');
		$this->assertCount(self::NET_ADDRS,$ao->downlinks());
	}

	public function test_hub_session_children()
	{
		$ao = Address::findFTN('101:10/100@a');

		// This is a HUB
		$this->assertEquals($ao->role_name,'HUB');
		$this->assertEquals(Address::NODE_HC,$ao->role_id);

		// Children
		$this->assertCount(self::HUB_ADDRS-1,$ao->children());
		$this->assertCount(0,$ao->downlinks());

		$this->session_hub();
		$ao->refresh()->unsetRelations();

		// This is a NC
		$this->assertCount(self::HUB_ADDRS,$ao->downlinks());
	}

	public function test_hub_parent()
	{
		$ao = Address::findFTN('101:10/100@a');
		$o = Address::findFTN('101:10/0@a');

		// This uplink should also be null
		$this->assertNull($ao->uplink()?->ftn);

		$this->session_hub();
		$ao->refresh();

		// This parent should also be null
		$this->assertEquals($o->ftn,$ao->parent()?->ftn);

		// This uplink should be itself
		$this->assertEquals($ao->ftn,$ao->uplink()?->ftn);
	}

	public function test_zc_rc_nc_hub_nodes()
	{
		$this->session_zc();
		$this->session_rc();
		$this->session_nc();
		$this->session_hub();

		$ao = Address::findFTN('101:0/0@a');
		$this->assertCount(self::ZONE_ADDRS-self::REGION_ADDRS,$ao->downlinks());

		$ao = Address::findFTN('101:1/0@a');
		$this->assertCount(self::REGION_ADDRS-self::NET_ADDRS,$ao->downlinks());

		$ao = Address::findFTN('101:10/0@a');
		$this->assertCount(self::NET_ADDRS-self::HUB_ADDRS,$ao->downlinks());

		$ao = Address::findFTN('101:10/100@a');
		$this->assertCount(self::HUB_ADDRS,$ao->downlinks());
	}

	public function test_node_session_children()
	{
		$ao = Address::findFTN('101:10/101@a');

		// This is a NC
		$this->assertEquals($ao->role_name,'NODE');
		$this->assertEquals(Address::NODE_NN,$ao->role_id);

		// Children
		$this->assertCount(self::NODE_ADDRS-1,$ao->children());
		$this->assertCount(0,$ao->downlinks());

		$this->session_node();
		$ao->refresh();

		// This is a NC
		$this->assertCount(self::NODE_ADDRS,$ao->downlinks());
	}

	public function test_node_parent()
	{
		$ao = Address::findFTN('101:10/101@a');
		$o = Address::findFTN('101:10/100@a');

		// This uplink should also be null
		$this->assertNull($ao->uplink()?->ftn);

		$this->session_node();
		$ao->refresh();

		// This parent should also be null
		$this->assertEquals($o->ftn,$ao->parent()?->ftn);

		// This uplink should be itself
		$this->assertEquals($ao->ftn,$ao->uplink()?->ftn);
	}

	public function test_zc_rc_nc_hub_node_nodes()
	{
		$this->session_zc();
		$this->session_rc();
		$this->session_nc();
		$this->session_hub();
		$this->session_node();

		$ao = Address::findFTN('101:0/0@a');
		$this->assertCount(self::ZONE_ADDRS-self::REGION_ADDRS,$ao->downlinks());

		$ao = Address::findFTN('101:1/0@a');
		$this->assertCount(self::REGION_ADDRS-self::NET_ADDRS,$ao->downlinks());

		$ao = Address::findFTN('101:10/0@a');
		$this->assertCount(self::NET_ADDRS-self::HUB_ADDRS,$ao->downlinks());

		$ao = Address::findFTN('101:10/100@a');
		$this->assertCount(self::HUB_ADDRS-self::NODE_ADDRS,$ao->downlinks());

		$ao = Address::findFTN('101:10/101@a');
		$this->assertCount(self::NODE_ADDRS,$ao->downlinks());
	}

	public function test_point_session_children()
	{
		$ao = Address::findFTN('101:10/101.1@a');

		// This is a NC
		$this->assertEquals($ao->role_name,'POINT');
		$this->assertEquals(Address::NODE_POINT,$ao->role_id);

		// Children
		$this->assertCount(0,$ao->children());
		$this->assertCount(0,$ao->downlinks());

		$this->session_point();
		$ao->refresh();

		// This is a NC
		$this->assertCount(1,$ao->downlinks());
	}

	public function test_point_parent()
	{
		$ao = Address::findFTN('101:10/101.1@a');
		$o = Address::findFTN('101:10/101@a');

		// This uplink should also be null
		$this->assertNull($ao->uplink()?->ftn);

		$this->session_point();
		$ao->refresh();

		// This parent should also be null
		$this->assertEquals($o->ftn,$ao->parent()?->ftn);

		// This uplink should be itself
		$this->assertEquals($ao->ftn,$ao->uplink()?->ftn);
	}
}