88 results

using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
using System.Text.Json;
using Hexagon.V2.Application;
using Hexagon.V2.Domain;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Hexagon.V2.Tests.Application;

[TestClass]
public sealed class CurrencyAndCharacterRulesTests
{
	[TestMethod]
	public void CurrencyRejectsUnderflowAndOverflow()
	{
		var character = CreateCharacter( 10 );
		Assert.IsTrue( CurrencyService.Debit( character, 11 ).Failed );
		Assert.IsTrue( CurrencyService.Credit( character with { Balance = long.MaxValue }, 1 ).Failed );
	}

	[TestMethod]
	public void SlotAllocatorUsesLowestGap()
	{
		var characters = new[] { CreateCharacter( 0 ) with { Slot = 0 }, CreateCharacter( 0 ) with { Slot = 2 } };
		Assert.AreEqual( 1, CharacterRules.FindLowestFreeSlot( characters ) );
	}

	[TestMethod]
	public void SlotAllocatorReturnsMinusOneWhenEverySlotIsOccupied()
	{
		var characters = Enumerable.Range( 0, CharacterRules.MaximumSlots )
			.Select( slot => CreateCharacter( 0 ) with { Slot = slot } )
			.ToArray();
		Assert.AreEqual( -1, CharacterRules.FindLowestFreeSlot( characters ) );

		var oneFree = characters.Where( character => character.Slot != CharacterRules.MaximumSlots - 1 ).ToArray();
		Assert.AreEqual( CharacterRules.MaximumSlots - 1, CharacterRules.FindLowestFreeSlot( oneFree ) );
	}

	[TestMethod]
	public void CreationRejectsUnregisteredFields()
	{
		var request = new CharacterCreationRequest
		{
			Name = "Valid Name",
			Description = "A sufficiently long character description.",
			Model = new DefinitionId( "test.model" ),
			Faction = new FactionId( "test.faction" ),
			Fields = new Dictionary<string, CreationValue> { ["money"] = CreationValue.Integer( long.MaxValue ) }
		};

		Assert.IsTrue( CharacterRules.ValidateCreationRequest( request, new HashSet<string>() ).Failed );
	}

	private static CharacterRecord CreateCharacter( long balance )
	{
		using var document = JsonDocument.Parse( "{}" );
		return new CharacterRecord
		{
			Id = CharacterId.New(),
			AccountId = new AccountId( 1 ),
			Slot = 0,
			Name = "Test",
			Description = "A sufficiently long description.",
			Model = new DefinitionId( "test.model" ),
			Faction = new FactionId( "test.faction" ),
			Balance = balance,
			CreatedAt = DateTimeOffset.UtcNow,
			LastPlayedAt = DateTimeOffset.UtcNow,
			SchemaState = new TypedPayload
			{
				TypeId = new PersistedTypeId( "test.character" ),
				TypeVersion = 1,
				Data = document.RootElement.Clone()
			}
		};
	}
}
#nullable enable

using Hexagon.V2.Client;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Networking;

namespace Hexagon.V2.Tests.Client;

[TestClass]
public sealed class ClientControllerTests
{
	[TestMethod]
	public async Task ControllerMapsEveryUiIntentToAnExplicitCommand()
	{
		var transport = new RecordingTransport();
		var controller = new HexClientController(transport);
		var characterId = CharacterId.New();
		var sourceId = InventoryId.New();
		var targetId = InventoryId.New();
		var itemId = ItemId.New();
		var actionId = new ActionId("use");
		var actionInstance = Guid.NewGuid();
		var actionArguments = new Dictionary<string, SnapshotValue>( StringComparer.Ordinal )
		{
			["amount"] = SnapshotValue.Integer( 3 )
		};
		var creation = new CharacterCreationInput(
			"Alyx", "Description", new DefinitionId("citizen_model"),
			new FactionId("citizen"), null);

		await controller.RequestCharactersAsync();
		await controller.CreateCharacterAsync(creation);
		await controller.LoadCharacterAsync(characterId);
		await controller.DeleteCharacterAsync(characterId);
		await controller.UnloadCharacterAsync();
		await controller.MoveItemAsync(sourceId, targetId, itemId, 2, 3);
		await controller.RunItemActionAsync(sourceId, itemId, actionId, actionArguments);
		actionArguments["amount"] = SnapshotValue.Integer( 99 );
		await controller.DropItemAsync(sourceId, itemId);
		await controller.PickUpItemAsync(itemId, targetId);
		await controller.SendChatAsync("ic", "Hello");
		await controller.CancelActionAsync(actionInstance);

		Assert.HasCount(11, transport.Commands);
		Assert.IsInstanceOfType<RequestCharacterListCommand>(transport.Commands[0]);
		Assert.AreSame(creation, ((CreateCharacterCommand)transport.Commands[1]).Input);
		Assert.AreEqual(characterId, ((LoadCharacterCommand)transport.Commands[2]).CharacterId);
		Assert.IsInstanceOfType<DeleteCharacterCommand>(transport.Commands[3]);
		Assert.IsInstanceOfType<UnloadCharacterCommand>(transport.Commands[4]);
		Assert.AreEqual((2, 3),
			(((MoveInventoryItemCommand)transport.Commands[5]).X,
				((MoveInventoryItemCommand)transport.Commands[5]).Y));
		Assert.AreEqual(actionId, ((RunItemActionCommand)transport.Commands[6]).ActionId);
		Assert.AreEqual( 3L, ((RunItemActionCommand)transport.Commands[6]).Arguments["amount"].IntegerValue );
		Assert.IsInstanceOfType<DropItemCommand>(transport.Commands[7]);
		Assert.IsInstanceOfType<PickUpItemCommand>(transport.Commands[8]);
		Assert.AreEqual("Hello", ((SendChatCommand)transport.Commands[9]).Text);
		Assert.AreEqual(actionInstance, ((CancelActionCommand)transport.Commands[10]).InstanceId);
	}

	[TestMethod]
	public async Task TransportFailureAndCancellationTokenArePropagated()
	{
		var expected = OperationResult.Failure(ErrorCode.Unauthorized, "Denied.");
		var transport = new RecordingTransport(expected);
		var controller = new HexClientController(transport);
		using var source = new CancellationTokenSource();

		var result = await controller.SendChatAsync("ic", "Hello", source.Token);

		Assert.AreEqual(ErrorCode.Unauthorized, result.Error!.Code);
		Assert.AreEqual(source.Token, transport.LastCancellationToken);
	}

	private sealed class RecordingTransport : IClientCommandTransport
	{
		private readonly OperationResult _result;

		public RecordingTransport(OperationResult? result = null) =>
			_result = result ?? OperationResult.Success();

		public List<ClientCommand> Commands { get; } = new();
		public CancellationToken LastCancellationToken { get; private set; }

		public ValueTask<OperationResult> SendAsync(
			ClientCommand command,
			CancellationToken cancellationToken = default)
		{
			Commands.Add(command);
			LastCancellationToken = cancellationToken;
			return ValueTask.FromResult(_result);
		}
	}
}
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;

using Microsoft.VisualStudio.TestTools.UnitTesting;

[assembly: Parallelize( Scope = ExecutionScope.MethodLevel )]
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace WackyLib.Tests;

[TestClass]
public class TestInit
{
	private static Sandbox.TestAppSystem s_appSystem;
	
	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		s_appSystem = new Sandbox.TestAppSystem();
		s_appSystem.Init();
	}
	
	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		s_appSystem.Shutdown();
	}
}
using Sandbox;

[TestClass]
public class SyncToolYamlRendererTests
{
	[TestMethod]
	public void EmptyInputProducesEmptyOutput()
	{
		Assert.AreEqual( "", SyncToolYamlRenderer.RenderFromJson( null ) );
		Assert.AreEqual( "", SyncToolYamlRenderer.RenderFromJson( "" ) );
	}

	[TestMethod]
	public void EmptyObjectAndArrayRenderAsFlowStyle()
	{
		Assert.AreEqual( "{}\n", SyncToolYamlRenderer.RenderFromJson( "{}" ) );
		Assert.AreEqual( "[]\n", SyncToolYamlRenderer.RenderFromJson( "[]" ) );
	}

	[TestMethod]
	public void InvalidJsonReturnsInputUnchanged()
	{
		var notJson = "not: real: json: ::";
		Assert.AreEqual( notJson, SyncToolYamlRenderer.RenderFromJson( notJson ) );
	}

	[TestMethod]
	public void TopLevelKeysAreSortedAlphabetically()
	{
		var json = "{\"zeta\":1,\"alpha\":2,\"mu\":3}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "alpha: 2\nmu: 3\nzeta: 1\n", yaml );
	}

	[TestMethod]
	public void NestedObjectsAreSortedRecursively()
	{
		var json = "{\"outer\":{\"zeta\":1,\"alpha\":2}}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "outer:\n  alpha: 2\n  zeta: 1\n", yaml );
	}

	[TestMethod]
	public void DifferentKeyOrdersProduceIdenticalOutput()
	{
		var a = "{\"slug\":\"hello\",\"method\":\"POST\",\"enabled\":true}";
		var b = "{\"enabled\":true,\"method\":\"POST\",\"slug\":\"hello\"}";

		Assert.AreEqual(
			SyncToolYamlRenderer.RenderFromJson( a ),
			SyncToolYamlRenderer.RenderFromJson( b )
		);
	}

	[TestMethod]
	public void OutputUsesYamlSyntaxNotJsonSyntax()
	{
		var json = "{\"name\":\"hooked\",\"enabled\":true,\"max\":42}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		// Smoke check: the rendered text must look like YAML, not JSON.
		// This is the regression we're guarding: prior to the fix the diff
		// view rendered structured data as JSON.
		StringAssert.DoesNotMatch( yaml, new System.Text.RegularExpressions.Regex( @"^\s*\{" ) );
		StringAssert.Contains( yaml, "enabled: true" );
		StringAssert.Contains( yaml, "max: 42" );
		StringAssert.Contains( yaml, "name: \"hooked\"" );
	}

	[TestMethod]
	public void StringValuesAreQuotedAndEscapedSafely()
	{
		var json = "{\"text\":\"a:b\\nc\"}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		// Strings go through JSON quoting which is also valid YAML.
		// The colon/newline must not leak as YAML structure.
		Assert.AreEqual( "text: \"a:b\\nc\"\n", yaml );
	}

	[TestMethod]
	public void BooleanAndNullAreUnquoted()
	{
		var json = "{\"a\":true,\"b\":false,\"c\":null}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "a: true\nb: false\nc: null\n", yaml );
	}

	[TestMethod]
	public void IntegerAndDoubleAreUnquoted()
	{
		var json = "{\"i\":7,\"d\":1.5}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "d: 1.5\ni: 7\n", yaml );
	}

	[TestMethod]
	public void ArraysOfObjectsRenderAsBlockSequence()
	{
		var json = "{\"steps\":[{\"name\":\"first\",\"id\":1},{\"name\":\"second\",\"id\":2}]}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		var expected =
			"steps:\n" +
			"  -\n" +
			"    id: 1\n" +
			"    name: \"first\"\n" +
			"  -\n" +
			"    id: 2\n" +
			"    name: \"second\"\n";

		Assert.AreEqual( expected, yaml );
	}

	[TestMethod]
	public void ArraysOfScalarsRenderAsBlockSequence()
	{
		var json = "{\"tags\":[\"a\",\"b\",\"c\"]}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "tags:\n  - \"a\"\n  - \"b\"\n  - \"c\"\n", yaml );
	}

	[TestMethod]
	public void TopLevelArrayRenders()
	{
		var json = "[1,2,3]";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "- 1\n- 2\n- 3\n", yaml );
	}

	[TestMethod]
	public void KeysWithNonIdentifierCharactersAreQuoted()
	{
		var json = "{\"weird key\":1,\"x:y\":2}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		// Bare YAML keys must not contain spaces or colons, so the renderer
		// quotes them. Sort order is by raw key (Ordinal).
		StringAssert.Contains( yaml, "\"weird key\": 1" );
		StringAssert.Contains( yaml, "\"x:y\": 2" );
	}

	[TestMethod]
	public void EmptyNestedObjectAndArrayUseFlowStyle()
	{
		var json = "{\"obj\":{},\"arr\":[]}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "arr: []\nobj: {}\n", yaml );
	}
}
#nullable enable

using Hexagon.V2.Application;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Kernel.Events;
using Hexagon.V2.Persistence;

namespace Hexagon.V2.Tests.Application;

[TestClass]
public sealed class SceneAndAggregateMutationTests
{
	[TestMethod]
	public async Task SceneStateUsesRegisteredTypesAndPublishesOnlyCommittedReplacement()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var service = new SceneEntityStateService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<SceneEntityStateMutationContext>());
		var sceneEntityId = SceneEntityId.New();
		var actor = new AccountId(9101);
		var initial = ApplicationServiceTestEnvironment.StatePayload("closed");

		var ensured = await service.EnsureAsync(sceneEntityId, "door", initial);
		var replaced = await service.ReplaceStateAsync(
			actor, null, sceneEntityId, ApplicationServiceTestEnvironment.StatePayload("open"));
		var incompatible = await service.ReplaceStateAsync(
			actor, null, sceneEntityId, ApplicationServiceTestEnvironment.StatePayload("future", version: 2));
		environment.Provider.FailNextCommit();
		var failedCommit = await service.ReplaceStateAsync(
			actor, null, sceneEntityId, ApplicationServiceTestEnvironment.StatePayload("unpublished"));

		Assert.IsTrue(ensured.Succeeded, ensured.Error?.Message);
		Assert.IsTrue(replaced.Succeeded, replaced.Error?.Message);
		Assert.AreEqual(ErrorCode.PersistedTypeInvalid, incompatible.Error!.Code);
		Assert.AreEqual(ErrorCode.InternalError, failedCommit.Error!.Code);
		var stored = service.Find(sceneEntityId)!;
		Assert.AreEqual("open", stored.State.Data.GetProperty("name").GetString());
		Assert.AreEqual(ApplicationServiceTestEnvironment.StateTypeId, stored.State.TypeId.Value);
		Assert.AreEqual(1, stored.State.TypeVersion);
	}

	[TestMethod]
	public async Task TouchLastPlayedReturnsOnlyProviderIssuedDurableReceipt()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId( 9103 );
		var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch
		};
		await environment.SeedAsync( unitOfWork =>
			unitOfWork.Create( environment.Repositories.Characters, DomainKeys.Character( character.Id ), character ) );
		var service = new AggregateMutationService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
			ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>() );

		var timestamp = DateTimeOffset.UnixEpoch.AddMinutes( 1 );
		var touched = await service.TouchLastPlayedAsync( account, character.Id, timestamp );

		Assert.IsTrue( touched.Succeeded, touched.Error?.Message );
		Assert.AreEqual( CharacterMutationKind.TouchLastPlayed, touched.Value.Kind );
		Assert.AreEqual( character.LastPlayedAt, touched.Value.Before.LastPlayedAt );
		Assert.AreEqual( timestamp, touched.Value.After.LastPlayedAt );
		Assert.IsGreaterThan( 0L, touched.Value.Commit.Sequence );
		Assert.IsTrue( touched.Value.Commit.Documents.Any( document =>
			document.Address.Collection == DomainCollections.Characters &&
			document.Address.Key == DomainKeys.Character( character.Id ) ) );

		environment.Provider.FailNextCommit();
		var failed = await service.TouchLastPlayedAsync(
			account, character.Id, timestamp.AddMinutes( 1 ) );

		Assert.IsTrue( failed.Failed );
		Assert.AreEqual( ErrorCode.InternalError, failed.Error!.Code );
		Assert.AreEqual( timestamp, environment.Repositories.Characters.Find(
			DomainKeys.Character( character.Id ) )!.Value.LastPlayedAt );
	}

	[TestMethod]
	public async Task PreparedTouchStagesIntoCombinedCommitAndPublishesOnlyOnMatchingCompletion()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId( 9104 );
		var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch
		};
		var item = ApplicationServiceTestEnvironment.Item();
		await environment.SeedAsync( unitOfWork =>
		{
			unitOfWork.Create(
				environment.Repositories.Characters, DomainKeys.Character( character.Id ), character );
			unitOfWork.Create( environment.Repositories.Items, DomainKeys.Item( item.Id ), item );
		} );
		var events = new RecordingCharacterChangedHandler();
		var service = new AggregateMutationService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
			ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>(),
			new PostCommitEventBus<CharacterChangedEvent>( new[]
			{
				new EventHandlerRegistration<CharacterChangedEvent>( "record", events )
			} ) );

		var timestamp = DateTimeOffset.UnixEpoch.AddMinutes( 1 );
		var prepared = service.PrepareTouchLastPlayed( account, character.Id, timestamp );
		Assert.IsTrue( prepared.Succeeded, prepared.Error?.Message );
		var unitOfWork = environment.Repositories.Provider.BeginUnitOfWork();
		Assert.IsTrue( service.StageTouchLastPlayed( unitOfWork, prepared.Value ).Succeeded );
		var itemDocument = environment.Repositories.Items.Find( DomainKeys.Item( item.Id ) )!;
		var itemEditor = unitOfWork.Edit( environment.Repositories.Items, itemDocument )!;
		itemEditor.Replace( itemEditor.Value with { Revision = 1 } );
		unitOfWork.Save( itemEditor );
		var committed = await unitOfWork.CommitAsync();
		await unitOfWork.DisposeAsync();
		Assert.IsTrue( committed.Succeeded, committed.Error?.Message );
		Assert.HasCount( 2, committed.Value!.Documents );
		Assert.IsEmpty( events.Events );

		var premature = service.CompleteTouchLastPlayed(
			prepared.Value, new CommitReceipt( committed.Value.Sequence, Array.Empty<CommittedDocumentVersion>() ) );
		Assert.AreEqual( ErrorCode.InvariantViolation, premature.Error!.Code );
		Assert.IsEmpty( events.Events );

		var completed = service.CompleteTouchLastPlayed( prepared.Value, committed.Value );
		Assert.IsTrue( completed.Succeeded, completed.Error?.Message );
		Assert.AreSame( committed.Value, completed.Value.Commit );
		Assert.AreEqual( timestamp, completed.Value.After.LastPlayedAt );
		Assert.HasCount( 1, events.Events );
		Assert.AreEqual( committed.Value.Sequence, events.Events[0].CommitSequence );
	}

	[TestMethod]
	public async Task CompletionSucceedsFromTheReceiptAloneAfterAnInterleavedCommit()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId( 9106 );
		var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch
		};
		await environment.SeedAsync( unitOfWork =>
			unitOfWork.Create(
				environment.Repositories.Characters, DomainKeys.Character( character.Id ), character ) );
		var events = new RecordingCharacterChangedHandler();
		var service = new AggregateMutationService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
			ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>(),
			new PostCommitEventBus<CharacterChangedEvent>( new[]
			{
				new EventHandlerRegistration<CharacterChangedEvent>( "record", events )
			} ) );

		var timestamp = DateTimeOffset.UnixEpoch.AddMinutes( 1 );
		var prepared = service.PrepareTouchLastPlayed( account, character.Id, timestamp );
		Assert.IsTrue( prepared.Succeeded, prepared.Error?.Message );
		var unitOfWork = environment.Repositories.Provider.BeginUnitOfWork();
		Assert.IsTrue( service.StageTouchLastPlayed( unitOfWork, prepared.Value ).Succeeded );
		var committed = await unitOfWork.CommitAsync();
		await unitOfWork.DisposeAsync();
		Assert.IsTrue( committed.Succeeded, committed.Error?.Message );

		var current = environment.Repositories.Characters.Find( DomainKeys.Character( character.Id ) )!;
		var competingUnit = environment.Repositories.Provider.BeginUnitOfWork();
		var competingEditor = competingUnit.Edit( environment.Repositories.Characters, current )!;
		competingEditor.Replace( competingEditor.Value with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch.AddMinutes( 2 )
		} );
		competingUnit.Save( competingEditor );
		Assert.IsTrue( (await competingUnit.CommitAsync()).Succeeded );
		await competingUnit.DisposeAsync();

		var completed = service.CompleteTouchLastPlayed( prepared.Value, committed.Value! );

		Assert.IsTrue( completed.Succeeded,
			"A durably committed mutation must complete from its receipt even after an interleaved commit." );
		Assert.AreEqual( timestamp, completed.Value.After.LastPlayedAt );
		Assert.HasCount( 1, events.Events );
	}

	[TestMethod]
	public async Task PreparedTouchCommitFailureAndRevisionConflictEmitNoEvent()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId( 9105 );
		var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch
		};
		await environment.SeedAsync( unitOfWork =>
			unitOfWork.Create(
				environment.Repositories.Characters, DomainKeys.Character( character.Id ), character ) );
		var events = new RecordingCharacterChangedHandler();
		var service = new AggregateMutationService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
			ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>(),
			new PostCommitEventBus<CharacterChangedEvent>( new[]
			{
				new EventHandlerRegistration<CharacterChangedEvent>( "record", events )
			} ) );

		var failedPlan = service.PrepareTouchLastPlayed(
			account, character.Id, DateTimeOffset.UnixEpoch.AddMinutes( 1 ) );
		var failedUnit = environment.Repositories.Provider.BeginUnitOfWork();
		Assert.IsTrue( service.StageTouchLastPlayed( failedUnit, failedPlan.Value ).Succeeded );
		environment.Provider.FailNextCommit();
		var failedCommit = await failedUnit.CommitAsync();
		await failedUnit.DisposeAsync();
		Assert.IsFalse( failedCommit.Succeeded );
		Assert.IsEmpty( events.Events );
		Assert.AreEqual( DateTimeOffset.UnixEpoch, environment.Repositories.Characters.Find(
			DomainKeys.Character( character.Id ) )!.Value.LastPlayedAt );

		var conflictedPlan = service.PrepareTouchLastPlayed(
			account, character.Id, DateTimeOffset.UnixEpoch.AddMinutes( 2 ) );
		var staleUnit = environment.Repositories.Provider.BeginUnitOfWork();
		Assert.IsTrue( service.StageTouchLastPlayed( staleUnit, conflictedPlan.Value ).Succeeded );
		var current = environment.Repositories.Characters.Find( DomainKeys.Character( character.Id ) )!;
		var competingUnit = environment.Repositories.Provider.BeginUnitOfWork();
		var competingEditor = competingUnit.Edit( environment.Repositories.Characters, current )!;
		competingEditor.Replace( competingEditor.Value with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch.AddMinutes( 3 )
		} );
		competingUnit.Save( competingEditor );
		var competingCommit = await competingUnit.CommitAsync();
		await competingUnit.DisposeAsync();
		Assert.IsTrue( competingCommit.Succeeded, competingCommit.Error?.Message );
		var conflict = await staleUnit.CommitAsync();
		await staleUnit.DisposeAsync();
		Assert.IsFalse( conflict.Succeeded );
		Assert.AreEqual( PersistenceErrorCode.RevisionConflict, conflict.Error!.Code );
		Assert.IsEmpty( events.Events );

		// Completion validates from the receipt alone; a receipt that does not contain the
		// prepared revision is rejected without publishing.
		var wrongCompletion = service.CompleteTouchLastPlayed(
			conflictedPlan.Value,
			new CommitReceipt( competingCommit.Value!.Sequence, Array.Empty<CommittedDocumentVersion>() ) );
		Assert.AreEqual( ErrorCode.InvariantViolation, wrongCompletion.Error!.Code );
		Assert.IsEmpty( events.Events );
	}

	[TestMethod]
	public async Task AggregateMutationFailuresLeaveTimestampBanBalanceStateAndTraitsUntouched()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId(9102);
		var character = ApplicationServiceTestEnvironment.Character(account, 0) with
		{
			Balance = 10,
			LastPlayedAt = DateTimeOffset.UnixEpoch.AddHours(2)
		};
		var item = ApplicationServiceTestEnvironment.Item() with
		{
			Traits = new Dictionary<string, TypedPayload>(StringComparer.Ordinal)
			{
				["state"] = ApplicationServiceTestEnvironment.StatePayload("old")
			}
		};
		await environment.SeedAsync(unitOfWork =>
		{
			unitOfWork.Create(environment.Repositories.Characters, DomainKeys.Character(character.Id), character);
			unitOfWork.Create(environment.Repositories.Items, DomainKeys.Item(item.Id), item);
		});
		var service = new AggregateMutationService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
			ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>());

		var oldTimestamp = await service.TouchLastPlayedAsync(
			account, character.Id, DateTimeOffset.UnixEpoch);
		var nonUtcBan = await service.SetBanAsync(
			account,
			character.Id,
			true,
			new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.FromHours(1)));
		var insufficientFunds = await service.DebitAsync(account, character.Id, 11);
		var unknownState = await service.ReplaceSchemaStateAsync(
			account,
			character.Id,
			ApplicationServiceTestEnvironment.StatePayload(typeId: "unknown.state"));
		var unknownTrait = await service.ReplaceTraitAsync(
			account,
			character.Id,
			item.Id,
			"state",
			ApplicationServiceTestEnvironment.StatePayload(typeId: "unknown.trait"));

		environment.Provider.FailNextCommit();
		var failedCredit = await service.CreditAsync(account, character.Id, 1);
		environment.Provider.FailNextCommit();
		var failedTraitCommit = await service.ReplaceTraitAsync(
			account,
			character.Id,
			item.Id,
			"state",
			ApplicationServiceTestEnvironment.StatePayload("new"));

		Assert.AreEqual(ErrorCode.InvalidArgument, oldTimestamp.Error!.Code);
		Assert.AreEqual(ErrorCode.InvalidArgument, nonUtcBan.Error!.Code);
		Assert.AreEqual(ErrorCode.Conflict, insufficientFunds.Error!.Code);
		Assert.AreEqual(ErrorCode.PersistedTypeInvalid, unknownState.Error!.Code);
		Assert.AreEqual(ErrorCode.PersistedTypeInvalid, unknownTrait.Error!.Code);
		Assert.AreEqual(ErrorCode.InternalError, failedCredit.Error!.Code);
		Assert.AreEqual(ErrorCode.InternalError, failedTraitCommit.Error!.Code);

		var storedCharacter = environment.Repositories.Characters.Find(DomainKeys.Character(character.Id))!.Value;
		var storedItem = environment.Repositories.Items.Find(DomainKeys.Item(item.Id))!.Value;
		Assert.AreEqual(character.LastPlayedAt, storedCharacter.LastPlayedAt);
		Assert.IsFalse(storedCharacter.IsBanned);
		Assert.IsNull(storedCharacter.BanExpiresAt);
		Assert.AreEqual(10L, storedCharacter.Balance);
		Assert.AreEqual("citizen", storedCharacter.SchemaState.Data.GetProperty("name").GetString());
		Assert.AreEqual("old", storedItem.Traits["state"].Data.GetProperty("name").GetString());
	}

	private sealed class RecordingCharacterChangedHandler : IEventHandler<CharacterChangedEvent>
	{
		public List<CharacterChangedEvent> Events { get; } = new();
		public void Handle( CharacterChangedEvent @event ) => Events.Add( @event );
	}
}
using Hexagon.V2.Application;
using Hexagon.V2.Domain;

namespace Hexagon.V2.Tests.Application;

[TestClass]
public sealed class SceneIdentityValidatorTests
{
	[TestMethod]
	public void ProvenanceClassifier_TrustsOnlyCapturedNonNetworkComponents()
	{
		var authored = Guid.NewGuid();
		var laterRuntime = Guid.NewGuid();
		var classifier = new SceneIdentityProvenanceClassifier();

		Assert.AreEqual(
			SceneIdentityProvenance.RuntimeOrNetwork,
			classifier.Classify( authored, hasActiveNetworkRoot: false ) );

		classifier.CaptureAuthoredSnapshot( new[] { authored } );

		Assert.IsTrue( classifier.HasAuthoredSnapshot );
		Assert.AreEqual( 1, classifier.AuthoredComponentCount );
		Assert.AreEqual(
			SceneIdentityProvenance.EditorAuthored,
			classifier.Classify( authored, hasActiveNetworkRoot: false ) );
		Assert.AreEqual(
			SceneIdentityProvenance.RuntimeOrNetwork,
			classifier.Classify( authored, hasActiveNetworkRoot: true ) );
		Assert.AreEqual(
			SceneIdentityProvenance.RuntimeOrNetwork,
			classifier.Classify( laterRuntime, hasActiveNetworkRoot: false ) );
	}

	[TestMethod]
	public void ProvenanceClassifier_NewSceneSnapshotReplacesPriorAuthority()
	{
		var firstScene = Guid.NewGuid();
		var secondScene = Guid.NewGuid();
		var classifier = new SceneIdentityProvenanceClassifier();
		classifier.CaptureAuthoredSnapshot( new[] { firstScene } );

		classifier.CaptureAuthoredSnapshot( new[] { secondScene } );

		Assert.AreEqual(
			SceneIdentityProvenance.RuntimeOrNetwork,
			classifier.Classify( firstScene, hasActiveNetworkRoot: false ) );
		Assert.AreEqual(
			SceneIdentityProvenance.EditorAuthored,
			classifier.Classify( secondScene, hasActiveNetworkRoot: false ) );
	}

	[TestMethod]
	public void EditorRepair_FillsMissingAndRepairsOnlyLaterDuplicate()
	{
		var duplicate = new SceneEntityId( Guid.Parse( "11111111-1111-1111-1111-111111111111" ) );
		var generated = new Queue<SceneEntityId>( new[]
		{
			new SceneEntityId( Guid.Parse( "22222222-2222-2222-2222-222222222222" ) ),
			new SceneEntityId( Guid.Parse( "33333333-3333-3333-3333-333333333333" ) )
		} );
		var result = SceneIdentityValidator.RepairForEditor( new[]
		{
			new SceneIdentityCandidate( "a", duplicate ),
			new SceneIdentityCandidate( "b", duplicate ),
			new SceneIdentityCandidate( "c", null )
		}, () => generated.Dequeue() );

		Assert.IsFalse( result[0].Repaired );
		Assert.IsTrue( result[1].Repaired );
		Assert.IsTrue( result[2].Repaired );
		Assert.AreNotEqual( result[0].EffectiveId, result[1].EffectiveId );
	}

	[TestMethod]
	public void RuntimeValidation_DisablesEveryAmbiguousOrBlankEntity()
	{
		var duplicate = new SceneEntityId( Guid.Parse( "11111111-1111-1111-1111-111111111111" ) );
		var result = SceneIdentityValidator.ValidateRuntime( new[]
		{
			new SceneIdentityCandidate( "a", duplicate ),
			new SceneIdentityCandidate( "b", duplicate ),
			new SceneIdentityCandidate( "c", null )
		} );

		Assert.IsTrue( result.All( value => !value.Enabled && value.FatalDiagnostic is not null ) );
	}

	[TestMethod]
	public void PersistentIndexEnumeratesTenThousandCandidatesOnceAndProvidesConstantTimeLookups()
	{
		var ids = Enumerable.Range( 0, 10_000 ).Select( _ => SceneEntityId.New() ).ToArray();
		var enumerated = 0;
		IEnumerable<SceneIdentityCandidate> Candidates()
		{
			for ( var index = 0; index < ids.Length; index++ )
			{
				enumerated++;
				yield return new SceneIdentityCandidate( $"entity/{index}", ids[index] );
			}
		}

		var index = PersistentSceneIdentityIndex.Build( Candidates() );

		Assert.AreEqual( 10_000, enumerated );
		Assert.AreEqual( 10_000, index.Count );
		for ( var candidate = 0; candidate < ids.Length; candidate++ )
		{
			Assert.IsTrue( index.TryResolveId( ids[candidate], out var byId ) );
			Assert.IsTrue( index.TryResolvePath( $"entity/{candidate}", out var byPath ) );
			Assert.AreSame( byId, byPath );
		}
	}

	[TestMethod]
	public void RuntimeNetworkDuplicateCannotPoisonEditorAuthoredIdentity()
	{
		var duplicate = SceneEntityId.New();
		var initial = PersistentSceneIdentityIndex.Build( new[]
		{
			new SceneIdentityCandidate( "first", duplicate )
		} );
		Assert.IsTrue( initial.TryResolveId( duplicate, out _ ) );

		var rebuilt = PersistentSceneIdentityIndex.Build( new[]
		{
			new SceneIdentityCandidate( "first", duplicate ),
			new SceneIdentityCandidate(
				"first", duplicate, SceneIdentityProvenance.RuntimeOrNetwork )
		} );

		Assert.IsTrue( rebuilt.TryResolveId( duplicate, out var resolved ) );
		Assert.AreEqual( "first", resolved.StablePath );
		Assert.IsTrue( rebuilt.Resolutions[0].Enabled );
		Assert.IsFalse( rebuilt.Resolutions[1].Enabled );
		Assert.AreEqual( SceneIdentityProvenance.RuntimeOrNetwork, rebuilt.Resolutions[1].Provenance );
		Assert.IsNull( rebuilt.Resolutions[1].EffectiveId );
		StringAssert.Contains( rebuilt.Resolutions[1].FatalDiagnostic, "runtime/network root" );
	}

	[TestMethod]
	public void TwoEditorAuthoredDuplicatesStillFailClosed()
	{
		var duplicate = SceneEntityId.New();
		var rebuilt = PersistentSceneIdentityIndex.Build( new[]
		{
			new SceneIdentityCandidate( "first", duplicate ),
			new SceneIdentityCandidate( "second", duplicate )
		} );

		Assert.IsFalse( rebuilt.TryResolveId( duplicate, out _ ) );
		Assert.IsTrue( rebuilt.Resolutions.All( resolution => !resolution.Enabled ) );
	}
}
using System.IO;

namespace Hexagon.V2.Tests.Foundation;

[TestClass]
public sealed class SandboxCompatibilityTests
{
	private static readonly string[] ForbiddenSourceTokens =
	[
		".ConfigureAwait(",
		"Volatile.",
		"ReaderWriterLockSlim",
		"CryptographicOperations.FixedTimeEquals",
		".IsInterface",
		".IsByRef",
		".IsPointer",
		".IsInstanceOfType",
		"ExceptionDispatchInfo",
		"System.Reflection",
		"RuntimeHelpers.",
		"Activator.CreateInstance",
		".GetProperties(",
		".GetFields(",
		".MakeGenericType(",
		".GetGenericArguments(",
		"await using",
		"Task.WhenAll(",
		"TaskScheduler",
		"Environment.ProcessId"
	];

	private static readonly (string Name, string Pattern)[] ForbiddenRawIoSignatures =
	[
		("File static API", @"\bFile\s*\."),
		("Directory static API", @"\bDirectory\s*\."),
		("Path.GetFullPath", @"\bPath\s*\.\s*GetFullPath\s*\("),
		("FileStream", @"\bFileStream\b"),
		("FileInfo", @"\bFileInfo\b"),
		("DirectoryInfo", @"\bDirectoryInfo\b"),
		("FileMode", @"\bFileMode\b"),
		("FileAccess", @"\bFileAccess\b"),
		("FileShare", @"\bFileShare\b"),
		("FileOptions", @"\bFileOptions\b"),
		("SearchOption", @"\bSearchOption\b")
	];

	[TestMethod]
	public void SandboxIndependentV2CodeAvoidsKnownWhitelistViolations()
	{
		var root = FindHexagonRoot();
		var sourceRoots = new[] { Path.Combine( root, "Code", "V2" ) }
			.Where( Directory.Exists )
			.ToArray();

		var violations = new List<string>();
		foreach ( var path in sourceRoots.SelectMany( sourceRoot =>
			Directory.GetFiles( sourceRoot, "*.cs", SearchOption.AllDirectories ) )
			.Where( path => !path.Contains(
				$"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}",
				StringComparison.OrdinalIgnoreCase ) ) )
		{
			var source = File.ReadAllText( path );
			if ( System.Text.RegularExpressions.Regex.IsMatch(
				source,
				@"^\s*(?:private|protected|internal|public)\s+(?:static\s+)?volatile\s+",
				System.Text.RegularExpressions.RegexOptions.Multiline ) )
				violations.Add(
					$"{Path.GetRelativePath( root, path )} declares a volatile field (lowers to forbidden IsVolatile)" );
			if ( System.Text.RegularExpressions.Regex.IsMatch(
				source,
				@"finally\s*\{[^{}]*\bawait\b",
				System.Text.RegularExpressions.RegexOptions.Singleline ) )
				violations.Add(
					$"{Path.GetRelativePath( root, path )} awaits inside a finally block (lowers to forbidden ExceptionDispatchInfo)" );
			if ( System.Text.RegularExpressions.Regex.IsMatch(
				source,
				@"catch(?:\s*\([^)]*\))?\s*\{[^{}]*\bawait\b[^{}]*\bthrow\s*;",
				System.Text.RegularExpressions.RegexOptions.Singleline ) )
				violations.Add(
					$"{Path.GetRelativePath( root, path )} awaits before a bare catch rethrow (lowers to forbidden ExceptionDispatchInfo)" );
			var lines = File.ReadAllLines( path );
			for ( var index = 0; index < lines.Length; index++ )
			{
				foreach ( var token in ForbiddenSourceTokens )
				{
					if ( lines[index].Contains( token, StringComparison.Ordinal ) )
						violations.Add( $"{Path.GetRelativePath( root, path )}:{index + 1} contains '{token}'" );
				}

				foreach ( var signature in FindForbiddenRawIoSignatures( lines[index] ) )
					violations.Add(
						$"{Path.GetRelativePath( root, path )}:{index + 1} uses forbidden raw I/O signature '{signature}'" );
			}
		}

		Assert.HasCount(
			0,
			violations,
			"Known s&box whitelist violations were found:" + Environment.NewLine + string.Join( Environment.NewLine, violations ) );
	}

	[TestMethod]
	public void RawIoSignatureGuardCoversOriginalSb1000SurfaceWithoutFalsePositives()
	{
		// Keep fixtures split so the production-source scanner can never match this test file if its roots expand.
		var separator = string.Empty;
		var forbiddenFixtures = new[]
		{
			"File" + separator + ".Exists( path )",
			"Path" + separator + ".GetFullPath( path )",
			"File" + separator + "Stream? lease",
			"File" + separator + "Info info",
			"Directory" + separator + "Info directory",
			"File" + separator + "Mode.OpenOrCreate",
			"File" + separator + "Access.ReadWrite",
			"File" + separator + "Share.None",
			"File" + separator + "Options.WriteThrough",
			"Search" + separator + "Option.AllDirectories",
			"Directory" + separator + " . EnumerateFiles( root )"
		};

		foreach ( var fixture in forbiddenFixtures )
			Assert.IsNotEmpty(
				FindForbiddenRawIoSignatures( fixture ),
				$"Expected raw I/O fixture to be rejected: {fixture}" );

		var allowedFixtures = new[]
		{
			"var path = issue.Path;",
			"var fileName = record.FileName;",
			"IReadOnlyList<string> directories",
			"storage.ExistsAsync( path, cancellationToken )"
		};

		foreach ( var fixture in allowedFixtures )
			Assert.IsEmpty(
				FindForbiddenRawIoSignatures( fixture ),
				$"Expected non-I/O fixture to remain allowed: {fixture}" );
	}

	private static string[] FindForbiddenRawIoSignatures( string sourceLine ) => ForbiddenRawIoSignatures
		.Where( signature => System.Text.RegularExpressions.Regex.IsMatch(
			sourceLine,
			signature.Pattern,
			System.Text.RegularExpressions.RegexOptions.CultureInvariant ) )
		.Select( signature => signature.Name )
		.ToArray();

	private static string FindHexagonRoot()
	{
		var directory = new DirectoryInfo( AppContext.BaseDirectory );
		while ( directory is not null && !File.Exists( Path.Combine( directory.FullName, "hexagon.sbproj" ) ) )
			directory = directory.Parent;

		Assert.IsNotNull( directory, "Could not locate the Hexagon repository root." );
		return directory.FullName;
	}
}
#nullable enable

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Networking;

namespace Hexagon.V2.Tests.Networking;

[TestClass]
public sealed class CommandDispatchOrchestratorTests
{
	[TestMethod]
	public void UnauthenticatedCallerIsRejectedBeforeAnyChargeOrResolution()
	{
		var host = new FakeDispatchHost { Authenticated = false };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.IsEmpty( host.Calls, "Unauthenticated traffic must never reach the per-connection bucket." );
		Assert.HasCount( 1, host.Sent );
		Assert.AreEqual( "caller", host.Sent[0].Target );
		Assert.AreEqual( ErrorCode.Unauthorized, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void ChargeHappensBeforeResolutionAndCarriesTheDeclaredCost()
	{
		var host = new FakeDispatchHost();

		CommandDispatchOrchestrator.Dispatch( host, Header(), 3, static () => new RequestCharacterListCommand() );

		Assert.AreEqual( "charge:3", host.Calls[0], "The caller is charged before its scope is inspected." );
		Assert.AreEqual( "resolve:initial", host.Calls[1] );
	}

	[TestMethod]
	public void RateLimitedAdmissionCarriesRetryAfterAndSkipsResolution()
	{
		var host = new FakeDispatchHost
		{
			Admission = CommandAdmissionResult.Reject(
				CommandAdmissionFailure.RateLimited, TimeSpan.FromSeconds( 2 ) )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 4, static () => new RequestCharacterListCommand() );

		CollectionAssert.AreEqual( new[] { "charge:4" }, host.Calls );
		Assert.HasCount( 1, host.Sent );
		Assert.AreEqual( ErrorCode.RateLimited, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void DuplicateRequestIdsConflict()
	{
		var host = new FakeDispatchHost
		{
			Admission = CommandAdmissionResult.Reject( CommandAdmissionFailure.Duplicate )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.AreEqual( ErrorCode.Conflict, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void ResolutionFailureIsForwardedAndTheAcceptedChargeIsClosed()
	{
		var host = new FakeDispatchHost
		{
			Resolution = OperationResult<string>.Failure( ErrorCode.Unauthorized, "scope mismatch" )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		CollectionAssert.AreEqual(
			new[] { "charge:1", "resolve:initial", "finish-rejected" },
			host.Calls );
		Assert.AreEqual( "scope mismatch", host.Sent[0].Result.Error!.Message );
	}

	[TestMethod]
	public void EmptyRequestIdCompletesWithInvalidArgument()
	{
		var host = new FakeDispatchHost();
		// Wire deserialization can materialize a default header; the constructor itself
		// rejects empty request ids, so the guard is only reachable through default structs.
		var header = default( ClientCommandHeader );

		CommandDispatchOrchestrator.Dispatch( host, header, 1, static () => new RequestCharacterListCommand() );

		CollectionAssert.Contains( host.Calls, "complete" );
		Assert.HasCount( 1, host.Sent );
		Assert.AreEqual( "actor", host.Sent[0].Target );
		Assert.AreEqual( ErrorCode.InvalidArgument, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void ThrowingCommandFactoryFailsClosedWithoutStartingAHostOperation()
	{
		var host = new FakeDispatchHost();

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1,
			static () => throw new FormatException( "bad wire payload" ) );

		Assert.HasCount( 1, host.MalformedPayloads );
		CollectionAssert.DoesNotContain( host.Calls, "host-operation" );
		Assert.AreEqual( ErrorCode.InvalidArgument, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void OversizedPayloadIsRejectedBeforeStableResolutionAndExecution()
	{
		var host = new FakeDispatchHost();

		CommandDispatchOrchestrator.Dispatch( host, Header(), 2,
			static () => new SendChatCommand( "ic", new string( 'a', 1_000_000 ) ) );

		CollectionAssert.DoesNotContain( host.Calls, "resolve:stable" );
		CollectionAssert.DoesNotContain( host.Calls, "host-operation" );
		Assert.HasCount( 1, host.Sent );
		Assert.IsFalse( host.Sent[0].Result.Succeeded );
	}

	[TestMethod]
	public void StableCharacterCommandsAreReResolvedAfterPayloadValidation()
	{
		var host = new FakeDispatchHost();

		CommandDispatchOrchestrator.Dispatch( host, Header(), 2,
			static () => new SendChatCommand( "ic", "hello" ) );

		CollectionAssert.AreEqual(
			new[]
			{
				"charge:2", "resolve:initial", "resolve:stable",
				"host-operation", "lease-check", "execute", "complete"
			},
			host.Calls );
		Assert.AreEqual( "stable-actor", host.Sent[0].Target,
			"Execution and completion must use the re-resolved stable actor." );
	}

	[TestMethod]
	public void StableResolutionFailureCompletesWithoutExecuting()
	{
		var host = new FakeDispatchHost
		{
			StableResolution = OperationResult<string>.Failure(
				ErrorCode.Unauthorized, "character changed" )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 2,
			static () => new SendChatCommand( "ic", "hello" ) );

		CollectionAssert.DoesNotContain( host.Calls, "execute" );
		Assert.AreEqual( "character changed", host.Sent[0].Result.Error!.Message );
	}

	[TestMethod]
	public void CharacterLifecycleCommandsSkipTheStableCharacterResolution()
	{
		foreach ( var factory in new Func<ClientCommand>[]
		{
			static () => new RequestCharacterListCommand(),
			static () => new LoadCharacterCommand( CharacterId.New() ),
			static () => new DeleteCharacterCommand( CharacterId.New() ),
			static () => new UnloadCharacterCommand()
		} )
		{
			var host = new FakeDispatchHost();
			CommandDispatchOrchestrator.Dispatch( host, Header(), 1, factory );
			CollectionAssert.DoesNotContain( host.Calls, "resolve:stable",
				$"{factory().GetType().Name} must dispatch without an active character." );
			Assert.AreEqual( "actor", host.Sent[0].Target );
		}
	}

	[TestMethod]
	public void HostOperationRefusalCompletesWithDrainingConflict()
	{
		var host = new FakeDispatchHost { AcceptHostOperation = false };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.AreEqual( ErrorCode.Conflict, host.Sent[0].Result.Error!.Code );
		StringAssert.Contains( host.StartedOperationName!, "command:" );
	}

	[TestMethod]
	public void StaleLeaseInsideTheHostOperationRejectsBeforeExecution()
	{
		var host = new FakeDispatchHost { LeaseCurrent = false };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		CollectionAssert.DoesNotContain( host.Calls, "execute" );
		Assert.AreEqual( ErrorCode.Unauthorized, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void MissingHostApplicationCompletesWithInternalError()
	{
		var host = new FakeDispatchHost { ExecutionAvailable = false };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		CollectionAssert.DoesNotContain( host.Calls, "execute" );
		Assert.AreEqual( ErrorCode.InternalError, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void ExecutionFaultBecomesInternalError()
	{
		var host = new FakeDispatchHost
		{
			Execution = static () => throw new InvalidOperationException( "handler exploded" )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.HasCount( 1, host.ExecutionFaults );
		Assert.AreEqual( ErrorCode.InternalError, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void ExecutionFaultUnderCancellationBecomesUnauthorized()
	{
		using var cancellation = new CancellationTokenSource();
		cancellation.Cancel();
		var host = new FakeDispatchHost
		{
			Cancellation = cancellation.Token,
			Execution = static () => throw new OperationCanceledException()
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.AreEqual( ErrorCode.Unauthorized, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void DisconnectedCompletionSendsNothing()
	{
		var host = new FakeDispatchHost { CompletionStatus = CommandCompletionStatus.Disconnected };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.IsEmpty( host.Sent );
	}

	[TestMethod]
	public void StaleCompletionKeepsASuccessfulExecutionResultButRejectsAFailedOne()
	{
		var success = new FakeDispatchHost { CompletionStatus = CommandCompletionStatus.Stale };
		CommandDispatchOrchestrator.Dispatch( success, Header(), 1, static () => new RequestCharacterListCommand() );
		Assert.IsTrue( success.Sent[0].Result.Succeeded,
			"A successful application result stays authoritative even under a stale lease." );

		var failure = new FakeDispatchHost
		{
			CompletionStatus = CommandCompletionStatus.Stale,
			Execution = static () => ValueTask.FromResult(
				OperationResult.Failure( ErrorCode.NotFound, "gone" ) )
		};
		CommandDispatchOrchestrator.Dispatch( failure, Header(), 1, static () => new RequestCharacterListCommand() );
		Assert.AreEqual( ErrorCode.Unauthorized, failure.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void SuccessfulExecutionForwardsTheApplicationResult()
	{
		var host = new FakeDispatchHost
		{
			Execution = static () => ValueTask.FromResult(
				OperationResult.Failure( ErrorCode.PolicyDenied, "application said no" ) )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.AreEqual( ErrorCode.PolicyDenied, host.Sent[0].Result.Error!.Code );
		Assert.AreEqual( "application said no", host.Sent[0].Result.Error!.Message );
	}

	[TestMethod]
	public void ThrowingCompletionHookIsObservedWithoutEscaping()
	{
		var host = new FakeDispatchHost { CompleteThrows = true };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.HasCount( 1, host.CompletionFaults );
		Assert.IsEmpty( host.Sent );
	}

	[TestMethod]
	public void CommandCostTableIsPinned()
	{
		var table = new Dictionary<string, int>( StringComparer.Ordinal )
		{
			[nameof( ClientCommandCosts.CharacterList )] = ClientCommandCosts.CharacterList,
			[nameof( ClientCommandCosts.CreateCharacter )] = ClientCommandCosts.CreateCharacter,
			[nameof( ClientCommandCosts.LoadCharacter )] = ClientCommandCosts.LoadCharacter,
			[nameof( ClientCommandCosts.DeleteCharacter )] = ClientCommandCosts.DeleteCharacter,
			[nameof( ClientCommandCosts.UnloadCharacter )] = ClientCommandCosts.UnloadCharacter,
			[nameof( ClientCommandCosts.MoveItem )] = ClientCommandCosts.MoveItem,
			[nameof( ClientCommandCosts.ItemAction )] = ClientCommandCosts.ItemAction,
			[nameof( ClientCommandCosts.DropItem )] = ClientCommandCosts.DropItem,
			[nameof( ClientCommandCosts.PickupItem )] = ClientCommandCosts.PickupItem,
			[nameof( ClientCommandCosts.Chat )] = ClientCommandCosts.Chat,
			[nameof( ClientCommandCosts.CancelAction )] = ClientCommandCosts.CancelAction,
			[nameof( ClientCommandCosts.BeginInteraction )] = ClientCommandCosts.BeginInteraction,
			[nameof( ClientCommandCosts.ContinueInteraction )] = ClientCommandCosts.ContinueInteraction,
			[nameof( ClientCommandCosts.CloseInteraction )] = ClientCommandCosts.CloseInteraction,
			[nameof( ClientCommandCosts.SchemaCommandFallback )] = ClientCommandCosts.SchemaCommandFallback
		};
		var expected = new Dictionary<string, int>( StringComparer.Ordinal )
		{
			["CharacterList"] = 1,
			["CreateCharacter"] = 4,
			["LoadCharacter"] = 4,
			["DeleteCharacter"] = 4,
			["UnloadCharacter"] = 1,
			["MoveItem"] = 4,
			["ItemAction"] = 4,
			["DropItem"] = 4,
			["PickupItem"] = 4,
			["Chat"] = 2,
			["CancelAction"] = 1,
			["BeginInteraction"] = 2,
			["ContinueInteraction"] = 1,
			["CloseInteraction"] = 1,
			["SchemaCommandFallback"] = 8
		};
		CollectionAssert.AreEquivalent( expected, table );
	}

	[TestMethod]
	public void OnlyCharacterLifecycleCommandsAreExemptFromTheStableCharacterRequirement()
	{
		Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter( new RequestCharacterListCommand() ) );
		Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter(
			new LoadCharacterCommand( CharacterId.New() ) ) );
		Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter(
			new DeleteCharacterCommand( CharacterId.New() ) ) );
		Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter( new UnloadCharacterCommand() ) );
		Assert.IsTrue( CommandDispatchOrchestrator.RequiresStableCharacter( new SendChatCommand( "ic", "hi" ) ) );
		Assert.IsTrue( CommandDispatchOrchestrator.RequiresStableCharacter(
			new CloseInteractionCommand( InteractionSessionId.New() ) ) );
	}

	private static ClientSessionScope Scope() =>
		new( ClientSessionNonce.New(), ConnectionEpoch.New() );

	private static ClientCommandHeader Header() =>
		new( Scope(), new CommandRequestId( Guid.NewGuid() ) );

	private sealed class FakeDispatchHost : ICommandDispatchHost<string>
	{
		public List<string> Calls { get; } = new();
		public bool Authenticated { get; set; } = true;
		public bool ExecutionAvailable { get; set; } = true;
		public CommandAdmissionResult Admission { get; set; } = CommandAdmissionResult.Success();
		public OperationResult<string> Resolution { get; set; } = OperationResult<string>.Success( "actor" );
		public OperationResult<string> StableResolution { get; set; } = OperationResult<string>.Success( "stable-actor" );
		public bool LeaseCurrent { get; set; } = true;
		public bool AcceptHostOperation { get; set; } = true;
		public bool CompleteThrows { get; set; }
		public Func<ValueTask<OperationResult>>? Execution { get; set; }
		public CancellationToken Cancellation { get; set; }
		public CommandCompletionStatus CompletionStatus { get; set; } = CommandCompletionStatus.Current;
		public List<(string Target, OperationResult Result)> Sent { get; } = new();
		public List<Exception> MalformedPayloads { get; } = new();
		public List<Exception> ExecutionFaults { get; } = new();
		public List<Exception> CompletionFaults { get; } = new();
		public string? StartedOperationName { get; private set; }

		public bool CallerIsAuthenticated => Authenticated;
		public bool IsExecutionAvailable => ExecutionAvailable;

		public CommandAdmissionResult TryBeginCommand( CommandRequestId requestId, int cost )
		{
			Calls.Add( $"charge:{cost}" );
			return Admission;
		}

		public OperationResult<string> ResolveActor( ClientSessionScope scope, bool requireStableCharacter )
		{
			Calls.Add( requireStableCharacter ? "resolve:stable" : "resolve:initial" );
			return requireStableCharacter ? StableResolution : Resolution;
		}

		public void FinishRejectedCommand( CommandRequestId requestId ) => Calls.Add( "finish-rejected" );

		public bool IsCommandLeaseCurrent( string actor )
		{
			Calls.Add( "lease-check" );
			return LeaseCurrent;
		}

		public bool TryStartHostOperation( string name, Func<Task> operation )
		{
			Calls.Add( "host-operation" );
			StartedOperationName = name;
			if ( !AcceptHostOperation ) return false;
			operation().GetAwaiter().GetResult();
			return true;
		}

		public CancellationToken CommandCancellation( string actor ) => Cancellation;

		public ValueTask<OperationResult> ExecuteAsync(
			string actor,
			ClientCommand command,
			CancellationToken cancellationToken )
		{
			Calls.Add( "execute" );
			return Execution?.Invoke() ?? ValueTask.FromResult( OperationResult.Success() );
		}

		public CommandCompletionStatus CompleteCommand( string actor, CommandRequestId requestId )
		{
			Calls.Add( "complete" );
			if ( CompleteThrows ) throw new InvalidOperationException( "completion registry exploded" );
			return CompletionStatus;
		}

		public void SendResultToCaller( ClientSessionScope scope, CommandRequestId requestId, OperationResult result ) =>
			Sent.Add( ("caller", result) );

		public void SendResult( string actor, CommandRequestId requestId, OperationResult result ) =>
			Sent.Add( (actor, result) );

		public void OnMalformedPayload( Exception exception ) => MalformedPayloads.Add( exception );
		public void OnExecutionFault( CommandRequestId requestId, Exception exception ) => ExecutionFaults.Add( exception );
		public void OnCompletionFault( CommandRequestId requestId, Exception exception ) => CompletionFaults.Add( exception );
	}
}
#nullable enable

using Hexagon.V2.Runtime;

namespace Hexagon.V2.Tests.Runtime;

[TestClass]
public sealed class HexMovementValidatorTests
{
	private const float RunSpeed = 320f;
	private const float JumpSpeed = 300f;
	private const float Dt = 1f / 30f;

	private static MovementSample At( float x, float y, float z ) => new( x, y, z );

	[TestMethod]
	public void AcceptsMovementWithinTheRunSpeedEnvelope()
	{
		// Horizontal envelope ≈ 320 * 1.25 * (1/30) + 4 ≈ 17 units per tick.
		var decision = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 9, 0, 0 ), Dt, RunSpeed, JumpSpeed, frozen: false );
		Assert.IsFalse( decision.Corrected );
	}

	[TestMethod]
	public void CorrectsHorizontalMovementBeyondTheEnvelope()
	{
		var decision = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 200, 0, 0 ), Dt, RunSpeed, JumpSpeed, frozen: false );
		Assert.IsTrue( decision.Corrected );
	}

	[TestMethod]
	public void CorrectsHardTeleportEvenAcrossALargeStep()
	{
		var decision = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 1000, 0, 0 ), 1f, RunSpeed, JumpSpeed, frozen: false );
		Assert.IsTrue( decision.Corrected );
	}

	[TestMethod]
	public void FreezeCorrectsMovementButToleratesSkinJitter()
	{
		var moved = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 40, 0, 0 ), Dt, RunSpeed, JumpSpeed, frozen: true );
		Assert.IsTrue( moved.Corrected );

		var jitter = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 2, 0, 0 ), Dt, RunSpeed, JumpSpeed, frozen: true );
		Assert.IsFalse( jitter.Corrected );
	}

	[TestMethod]
	public void CorrectsNonFinitePosition()
	{
		var decision = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( float.NaN, 0, 0 ), Dt, RunSpeed, JumpSpeed, frozen: false );
		Assert.IsTrue( decision.Corrected );
	}

	[TestMethod]
	public void AcceptsAFallWithinTheTerminalEnvelope()
	{
		// Falling envelope ≈ 1800 * (1/30) + 4 ≈ 64 units per tick.
		var decision = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 0, 0, -40 ), Dt, RunSpeed, JumpSpeed, frozen: false );
		Assert.IsFalse( decision.Corrected );
	}

	[TestMethod]
	public void CorrectsImpossibleVerticalRise()
	{
		// Rise envelope ≈ 300 * 1.5 * (1/30) + 4 ≈ 19 units per tick (no step-rise without motion).
		var decision = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 0, 0, 200 ), Dt, RunSpeed, JumpSpeed, frozen: false );
		Assert.IsTrue( decision.Corrected );
	}

	[TestMethod]
	public void RejectsHorizontalSpeedTheLooseSkinWouldHaveAllowed()
	{
		// 24 units/tick: within the old +16 skin (≈29), beyond the tightened +4 skin (≈17).
		var decision = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 24, 0, 0 ), Dt, RunSpeed, JumpSpeed, frozen: false );
		Assert.IsTrue( decision.Corrected );
	}

	[TestMethod]
	public void FrozenPlayerIsCorrectedBeyondTheTightFrozenSkin()
	{
		// 10 units/tick while frozen: the loose skin tolerated it; the frozen skin (2) does not.
		var decision = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 10, 0, 0 ), Dt, RunSpeed, JumpSpeed, frozen: true );
		Assert.IsTrue( decision.Corrected );
	}

	[TestMethod]
	public void RejectsStraightUpFlightWithoutHorizontalMotion()
	{
		// 30 units of pure vertical rise: with no horizontal motion there is no step allowance, so
		// only jump physics apply (≈19/tick) and this is corrected — a client cannot fly straight up.
		var decision = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 0, 0, 30 ), Dt, RunSpeed, JumpSpeed, frozen: false );
		Assert.IsTrue( decision.Corrected );
	}

	[TestMethod]
	public void AllowsAStepUpWhileMovingHorizontally()
	{
		// An 18-unit rise is legitimate when paired with horizontal movement (a stair/slope), so the
		// discrete step allowance keeps it smooth.
		var decision = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 6, 0, 18 ), Dt, RunSpeed, JumpSpeed, frozen: false );
		Assert.IsFalse( decision.Corrected );
	}

	[TestMethod]
	public void AcceptsANormalJumpRise()
	{
		var decision = HexMovementValidator.Evaluate(
			At( 0, 0, 0 ), At( 0, 0, 10 ), Dt, RunSpeed, JumpSpeed, frozen: false );
		Assert.IsFalse( decision.Corrected );
	}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
#nullable enable

using System;
using System.Linq;
using Hexagon.V2.Infrastructure;
using Hexagon.V2.Persistence;

namespace Hexagon.V2.Tests.Infrastructure;

[TestClass]
public sealed class PrefixedPersistenceStorageTests
{
	[TestMethod]
	public async Task EveryOperationIsMappedBelowTheIsolatedPrefix()
	{
		var physical = new InMemoryPersistenceStorage();
		var isolated = new PrefixedPersistenceStorage( physical, "verification/run-42" );

		Assert.IsTrue( await isolated.TryWriteImmutableAsync( "hexagon/v2/schema/format.json", new byte[] { 1, 2 } ) );
		Assert.IsTrue( await physical.ExistsAsync( "verification/run-42/hexagon/v2/schema/format.json" ) );
		Assert.IsFalse( await physical.ExistsAsync( "hexagon/v2/schema/format.json" ) );

		var listed = await isolated.ListAsync( "hexagon/v2/schema" );
		Assert.HasCount( 1, listed );
		Assert.AreEqual( "hexagon/v2/schema/format.json", listed.Single() );
	}

	[TestMethod]
	public void PrefixRejectsParentAndCurrentDirectorySegments()
	{
		var storage = new InMemoryPersistenceStorage();
		Assert.ThrowsExactly<ArgumentException>( () => new PrefixedPersistenceStorage( storage, "../escape" ) );
		Assert.ThrowsExactly<ArgumentException>( () => new PrefixedPersistenceStorage( storage, "safe/./escape" ) );
	}
}
#nullable enable

using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Networking;

namespace Hexagon.V2.Tests.Networking;

[TestClass]
public sealed class ClientPayloadLimitsTests
{
	[TestMethod]
	public void NullReferencePayloadsFailClosedInsteadOfEscapingAdmissionCleanup()
	{
		Assert.AreEqual(
			ErrorCode.InvalidArgument,
			ClientPayloadLimits.Validate( new CreateCharacterCommand( null! ) ).Error!.Code );
		Assert.AreEqual(
			ErrorCode.InvalidArgument,
			ClientPayloadLimits.Validate( new BeginInteractionCommand( null! ) ).Error!.Code );
	}

	[TestMethod]
	public void MapEntryAndPerStringBoundariesAreEnforced()
	{
		var accepted = Enumerable.Range( 0, ClientPayloadLimits.MaximumMapEntries )
			.ToDictionary( index => $"k{index}", _ => SnapshotValue.String( "x" ), StringComparer.Ordinal );
		var rejected = new Dictionary<string, SnapshotValue>( accepted, StringComparer.Ordinal )
		{
			["overflow"] = SnapshotValue.String( "x" )
		};

		Assert.IsTrue( ClientPayloadLimits.Validate( new RunSchemaCommandCommand( "test", accepted ) ).Succeeded );
		Assert.AreEqual( ErrorCode.InvalidArgument,
			ClientPayloadLimits.Validate( new RunSchemaCommandCommand( "test", rejected ) ).Error!.Code );
		Assert.IsTrue( ClientPayloadLimits.Validate(
			new SendChatCommand( "ic", new string( 'a', ClientPayloadLimits.MaximumStringCharacters ) ) ).Succeeded );
		Assert.IsTrue( ClientPayloadLimits.Validate(
			new SendChatCommand( "ic", new string( 'a', ClientPayloadLimits.MaximumStringCharacters + 1 ) ) ).Failed );
	}

	[TestMethod]
	public void InvalidUnicodeAndAggregateUtf8OverflowAreRejected()
	{
		var invalidUnicode = new SendChatCommand( "ic", "\ud800" );
		var fields = new Dictionary<string, SnapshotValue>( StringComparer.Ordinal );
		for ( var index = 0; index < 5; index++ )
			fields[$"key{index}"] = SnapshotValue.String( new string( '\u0800', 4096 ) );

		Assert.IsTrue( ClientPayloadLimits.Validate( invalidUnicode ).Failed );
		Assert.IsTrue( ClientPayloadLimits.Validate(
			new RunSchemaCommandCommand( "expensive", fields ) ).Failed );
	}

	[TestMethod]
	public void CreationIdentifiersContributeToAggregateUtf8Budget()
	{
		var fields = Enumerable.Range( 0, 4 ).ToDictionary(
			index => $"field{index}",
			_ => SnapshotValue.String( new string( 'x', 4090 ) ),
			StringComparer.Ordinal );
		var input = new CharacterCreationInput(
			"name",
			"description",
			new DefinitionId( new string( 'm', 96 ) ),
			new FactionId( new string( 'f', 96 ) ),
			new ClassId( new string( 'c', 96 ) ),
			fields );

		Assert.IsTrue( ClientPayloadLimits.Validate( new CreateCharacterCommand( input ) ).Failed );
	}
}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
#nullable enable annotations

using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using LichessNET.API;
using LichessNET.Entities.Board;
using LichessNET.Entities.Enumerations;
using LichessNET.Entities.OAuth;
using LichessNET.Gameplay;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public sealed class LichessGameplayTests
{
    [TestMethod]
    public async Task QueueSubscribesBeforeStartingAccountStream()
    {
        var stream = new FakeBoardStream();
        var client = new FakeBoardClient(stream);
        stream.OnStart = () => stream.Emit(
            "{\"type\":\"gameStart\",\"game\":{\"gameId\":\"game-1\",\"color\":\"white\"}}");

        LichessGameFoundEventArgs? found = null;
        await using var queue = new LichessQueue(client);
        queue.OnGameFound += (_, game) => found = game;

        await queue.StartSeekAsync(new BoardSeekOptions());

        Assert.IsTrue(stream.Started);
        Assert.IsNotNull(found);
        Assert.AreEqual("game-1", found.GameId);
        Assert.AreEqual(LichessQueueState.GameFound, queue.State);
        Assert.IsFalse(queue.IsSeeking);
    }

    [TestMethod]
    public async Task QueueCancellationReleasesStreamAndReturnsToIdle()
    {
        var stream = new FakeBoardStream();
        var client = new FakeBoardClient(stream);
        using var cancellation = new CancellationTokenSource();
        await using var queue = new LichessQueue(client);

        await queue.StartSeekAsync(new BoardSeekOptions(), cancellation.Token);
        Assert.AreEqual(LichessQueueState.Seeking, queue.State);

        cancellation.Cancel();
        stream.Complete();

        Assert.AreEqual(LichessQueueState.Idle, queue.State);
        Assert.IsFalse(queue.IsSeeking);
        Assert.IsTrue(stream.Disposed);
    }

    [TestMethod]
    public async Task SessionReconcilesPendingAndRebuildsDivergentHistory()
    {
        var first = new FakeBoardStream();
        var second = new FakeBoardStream();
        var client = new FakeBoardClient(new FakeBoardStream(), first, second);
        first.OnStart = () => first.Emit(GameFull("e2e4 e7e5", true));
        second.OnStart = () => second.Emit(GameFull("e2e4 e7e5 g1f3", false));

        var adapter = new RecordingChessBoardAdapter();
        await using var session = new LichessGameSession(
            client, "game-1", "white", adapter,
            new LichessGameSessionOptions { AutoReconnect = false });

        await session.StartAsync();

        Assert.AreEqual("standard", session.GameFull?.Variant);
        Assert.AreEqual("Blitz", session.GameFull?.Perf);
        Assert.IsTrue(session.WhiteOfferingDraw);
        CollectionAssert.AreEqual(
            new[] { "e2e4", "e7e5" }, new List<string>(session.MoveHistory));

        Assert.IsTrue(await session.SubmitLocalMoveAsync("g1f3"));
        Assert.AreEqual("g1f3", session.PendingLocalMove);

        await session.ReconnectAsync();

        Assert.IsNull(session.PendingLocalMove);
        CollectionAssert.AreEqual(
            new[] { "e2e4", "e7e5", "g1f3" },
            new List<string>(session.MoveHistory));

        second.Emit(
            "{\"type\":\"gameState\",\"moves\":\"d2d4\",\"status\":\"started\"}");

        CollectionAssert.AreEqual(
            new[] { "d2d4" }, new List<string>(session.MoveHistory));
        CollectionAssert.AreEqual(
            new[] { "d2d4" }, new List<string>(adapter.Moves));
    }

    [TestMethod]
    public async Task UnsupportedInitialFenBlocksLaterStateUpdates()
    {
        var stream = new FakeBoardStream();
        var client = new FakeBoardClient(new FakeBoardStream(), stream);
        stream.OnStart = () => stream.Emit(GameFull("e2e4", false, "8/8/8/8/8/8/8/8 w - - 0 1"));

        var adapter = new RecordingChessBoardAdapter();
        await using var session = new LichessGameSession(
            client, "game-1", "white", adapter,
            new LichessGameSessionOptions { AutoReconnect = false });

        await session.StartAsync();
        stream.Emit(
            "{\"type\":\"gameState\",\"moves\":\"e2e4 e7e5\",\"status\":\"started\"}");

        Assert.AreEqual(0, session.MoveHistory.Count);
        Assert.AreEqual(0, adapter.Moves.Count);
    }

    [TestMethod]
    public async Task AuthenticationFailureStopsAutomaticReconnect()
    {
        var first = new FakeBoardStream();
        var second = new FakeBoardStream();
        var client = new FakeBoardClient(new FakeBoardStream(), first, second);
        first.OnStart = () => first.Emit(GameFull(string.Empty, false));

        await using var session = new LichessGameSession(
            client, "game-1", "white", new RecordingChessBoardAdapter(),
            new LichessGameSessionOptions
            {
                AutoReconnect = true,
                ReconnectDelays = new[] { TimeSpan.Zero }
            });

        await session.StartAsync();
        first.Fail(new LichessApiException(HttpStatusCode.Unauthorized));
        first.Complete();

        Assert.IsFalse(second.Started);
        Assert.AreEqual(LichessGameConnectionState.Disconnected, session.ConnectionState);
    }

    [TestMethod]
    public async Task ZeroDelayReconnectStartsOnlyOneReplacementStream()
    {
        var first = new FakeBoardStream();
        var second = new FakeBoardStream();
        var client = new FakeBoardClient(new FakeBoardStream(), first, second);
        first.OnStart = () => first.Emit(GameFull(string.Empty, false));
        second.OnStart = () => second.Emit(GameFull(string.Empty, false));

        await using var session = new LichessGameSession(
            client, "game-1", "white", new RecordingChessBoardAdapter(),
            new LichessGameSessionOptions
            {
                AutoReconnect = true,
                ReconnectDelays = new[] { TimeSpan.Zero }
            });

        var unexpectedCompletions = 0;
        session.OnUnexpectedCompletion += _ => unexpectedCompletions++;

        await session.StartAsync();
        first.Complete();

        Assert.IsTrue(second.Started);
        Assert.AreEqual(1, unexpectedCompletions);
        Assert.AreEqual(LichessGameConnectionState.Connected, session.ConnectionState);
    }

    private static string GameFull(string moves, bool whiteDraw,
        string initialFen = "startpos")
    {
        return "{" +
               "\"type\":\"gameFull\"," +
               "\"id\":\"game-1\"," +
               "\"initialFen\":\"" + initialFen + "\"," +
               "\"variant\":{\"key\":\"standard\",\"name\":\"Standard\"}," +
               "\"speed\":\"blitz\"," +
               "\"perf\":{\"name\":\"Blitz\"}," +
               "\"state\":{" +
               "\"type\":\"gameState\"," +
               "\"moves\":\"" + moves + "\"," +
               "\"status\":\"started\"," +
               "\"wdraw\":" + whiteDraw.ToString().ToLowerInvariant() +
               "}}";
    }

    private sealed class FakeBoardStream : ILichessBoardEventStream
    {
        public event Action<ILichessBoardEventStream, JsonElement>? LineReceived;
        public event Action<ILichessBoardEventStream, Exception>? ErrorReceived;
        public event Action<ILichessBoardEventStream>? Completed;

        public Action? OnStart { get; set; }
        public bool Started { get; private set; }
        public bool Disposed { get; private set; }
        public Task Completion => Task.CompletedTask;

        public void Start()
        {
            Started = true;
            OnStart?.Invoke();
        }

        public void Emit(string json)
        {
            var element = JsonSerializer.Deserialize<JsonElement>(json);
            LineReceived?.Invoke(this, element);
        }

        public void Fail(Exception exception)
        {
            ErrorReceived?.Invoke(this, exception);
        }

        public void Complete()
        {
            Completed?.Invoke(this);
        }

        public ValueTask DisposeAsync()
        {
            Disposed = true;
            return ValueTask.CompletedTask;
        }
    }

    private sealed class FakeBoardClient : ILichessBoardClient
    {
        private readonly Queue<ILichessBoardEventStream> _gameStreams = new();
        private readonly ILichessBoardEventStream _accountStream;

        public FakeBoardClient(ILichessBoardEventStream accountStream,
            params ILichessBoardEventStream[] gameStreams)
        {
            _accountStream = accountStream;
            foreach (var stream in gameStreams)
                _gameStreams.Enqueue(stream);
        }

        public string? GetToken() => "test-token";

        public Task<Dictionary<string, TokenInfo?>> TestTokensAsync(
            List<string> tokens, CancellationToken cancellationToken = default)
        {
            var info = new TokenInfo
            {
                Permissions = new List<TokenPermission> { TokenPermission.PlayGames }
            };
            return Task.FromResult(new Dictionary<string, TokenInfo?>
            {
                ["test-token"] = info
            });
        }

        public Task CreateBoardSeekAsync(BoardSeekOptions options,
            CancellationToken cancellationToken = default)
        {
            return cancellationToken.IsCancellationRequested
                ? Task.FromCanceled(cancellationToken)
                : Task.CompletedTask;
        }

        public Task<ILichessBoardEventStream> CreateBoardAccountEventStreamAsync(
            CancellationToken cancellationToken = default)
        {
            return Task.FromResult(_accountStream);
        }

        public Task<ILichessBoardEventStream> CreateBoardGameStreamAsync(
            string gameId, CancellationToken cancellationToken = default)
        {
            return Task.FromResult(_gameStreams.Dequeue());
        }

        public Task<bool> MakeBoardMoveAsync(string gameId, string uci,
            bool offerDraw = false, CancellationToken cancellationToken = default)
        {
            return Task.FromResult(true);
        }

        public Task<bool> AbortBoardGameAsync(string gameId,
            CancellationToken cancellationToken = default) => Task.FromResult(true);

        public Task<bool> ResignBoardGameAsync(string gameId,
            CancellationToken cancellationToken = default) => Task.FromResult(true);

        public Task<bool> HandleDrawOfferAsync(string gameId, bool accept,
            CancellationToken cancellationToken = default) => Task.FromResult(true);

        public Task<bool> SendBoardChatAsync(string gameId, string text,
            BoardChatRoom room = BoardChatRoom.Player,
            CancellationToken cancellationToken = default) => Task.FromResult(true);
    }
}
#nullable enable

using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using Hexagon.V2.Networking;

namespace Hexagon.V2.Tests.Architecture;

[TestClass]
public sealed class LayerBoundaryTests
{
	private static readonly Regex BlockComments = new(@"/\*.*?\*/", RegexOptions.Singleline | RegexOptions.Compiled);
	private static readonly Regex LineComments = new(@"//.*?$", RegexOptions.Multiline | RegexOptions.Compiled);
	private static readonly Regex SyncAttribute = new(@"\[\s*Sync(?<body>[^\]]*)\]", RegexOptions.Compiled);
	private static readonly Regex AuthorityApi = new(
		@"\bRpc\.|\[\s*Rpc\b|\[\s*Sync\b|\bSyncFlags\.|\bFileSystem\.Data\b",
		RegexOptions.Compiled);

	public TestContext TestContext { get; set; } = null!;

	[TestMethod]
	public void DomainAndApplicationAreSandboxIndependent()
	{
		var violations = ProductFiles("Domain", "Application")
			.Select(file => (File: file, Source: SourceWithoutComments(file)))
			.Where(value => ContainsAny(value.Source, "using Sandbox", "Sandbox.") ||
				AuthorityApi.IsMatch(value.Source))
			.Select(value => Relative(value.File))
			.ToArray();

		Assert.IsEmpty(violations,
			$"Domain/Application must remain Sandbox-independent: {string.Join(", ", violations)}");
	}

	[TestMethod]
	public void SandboxAndAuthorityApisAreRestrictedToRuntimeAndInfrastructure()
	{
		var violations = ProductFiles()
			.Select(file => (File: file, Source: SourceWithoutComments(file)))
			.Where(value => ContainsAny(value.Source, "using Sandbox", "Sandbox.") ||
				AuthorityApi.IsMatch(value.Source))
			.Where(value => !IsAllowedSandboxLayer(value.File))
			.Select(value => Relative(value.File))
			.ToArray();

		Assert.IsEmpty(violations,
			$"Sandbox/authority APIs are restricted to Runtime and Infrastructure: {string.Join(", ", violations)}");
	}

	[TestMethod]
	public void EveryV2SyncAttributeIsExplicitlyHostAuthored()
	{
		var violations = new List<string>();
		foreach (var file in ProductFiles())
		{
			var source = SourceWithoutComments(file);
			foreach (Match match in SyncAttribute.Matches(source))
			{
				if (!match.Groups["body"].Value.Contains("SyncFlags.FromHost", StringComparison.Ordinal))
					violations.Add($"{Relative(file)}: {match.Value}");
			}
		}

		Assert.IsEmpty(violations,
			$"Every v2 synchronized field must be host-authored: {string.Join(" | ", violations)}");
	}

	[TestMethod]
	public void ReplicatedPlayerBodyIsPresentationOnlyAndNeverWritesTheClientStore()
	{
		var playerBody = Path.Combine(V2Root(), "Runtime", "HexPlayerBody.cs");
		var source = SourceWithoutComments(playerBody);
		var forbidden = new[] { "HexClientStore", "ClientStore", "ApplyState(", "ReplacePlayer(" };
		var violations = forbidden.Where(value => source.Contains(value, StringComparison.Ordinal)).ToArray();

		Assert.IsEmpty(violations,
			$"Replicated player state must remain presentation-only: {string.Join(", ", violations)}");
	}

	[TestMethod]
	public void PlayerIsASingleConnectionOwnedObjectRunningANativeController()
	{
		var playerBody = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexPlayerBody.cs" ) );
		// The player is one connection-owned object running a native, owner-simulated
		// PlayerController. This is the engine idiom (ownership == authority) that keeps the
		// body from being pinned to the world origin by host physics.
		StringAssert.Contains( playerBody, "controller.UseInputControls = true" );
		StringAssert.Contains( playerBody, "controller.UseLookControls = true" );
		StringAssert.Contains( playerBody, "controller.EnablePressing = true" );
		// No separate unowned body, and no revived custom client predictor.
		Assert.IsFalse( playerBody.Contains( "Owner = null!", StringComparison.Ordinal ),
			"The player body is the connection-owned object; no unowned host-simulated body may be spawned." );
		Assert.IsFalse( playerBody.Contains( "NetworkMode = NetworkMode.Never", StringComparison.Ordinal ),
			"The custom client predictor is removed in favour of the native controller's prediction." );

		var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
		StringAssert.Contains( runtime, "playerObject.NetworkSpawn( connection )" );
	}

	[TestMethod]
	public void ClientInputCannotBecomeSpatialAuthority()
	{
		var playerBody = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexPlayerBody.cs" ) );
		// Movement is owner-simulated, but position AUTHORITY stays on the host: it validates
		// the owner-reported transform against the controller's speed envelope and corrects
		// only through a host-authored channel. A client can never mint spatial authority.
		StringAssert.Contains( playerBody, "Sandbox.Networking.IsHost && GameObject.Network.IsProxy && IsEmbodied" );
		StringAssert.Contains( playerBody, "HostValidateMovement" );
		StringAssert.Contains( playerBody, "HexMovementValidator.Evaluate" );
		// The correction pulse is the only authority write to position, and it is host-authored.
		StringAssert.Contains( playerBody, "[Sync( SyncFlags.FromHost )] public Vector3 AuthoritativePosition" );
		StringAssert.Contains( playerBody, "[Sync( SyncFlags.FromHost )] public int CorrectionTick" );
		// The owner APPLIES corrections; it does not author them.
		StringAssert.Contains( playerBody, "GameObject.WorldPosition = AuthoritativePosition" );
		StringAssert.Contains( playerBody, "AuthoritativeBody" );
		// Gameplay resolves spatial checks against the host-validated position (not the raw client
		// transform), and a client that keeps reporting out-of-envelope positions is enforced
		// against — kicked, not merely nudged.
		StringAssert.Contains( playerBody, "public Vector3 AuthoritativeWorldPosition" );
		StringAssert.Contains( playerBody, "connection?.Kick(" );
		// The deleted owner->host input pump must not return in any form.
		Assert.IsFalse( playerBody.Contains( "SubmitInputFrame", StringComparison.Ordinal ),
			"Movement is owner-simulated; there must be no owner->host input RPC." );
		Assert.IsFalse( playerBody.Contains( "PlayableBody", StringComparison.Ordinal ) );

		var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
		StringAssert.Contains( runtime, "connection.CanSpawnObjects = false" );
		StringAssert.Contains( runtime, "connection.CanRefreshObjects = false" );
		StringAssert.Contains( runtime, "connection.CanDestroyObjects = false" );
	}

	[TestMethod]
	public void ProjectNetworkingAndPredictionCollisionDefaultsAreFailClosed()
	{
		var networking = File.ReadAllText( Path.Combine( ProductRoot(), "ProjectSettings", "Networking.config" ) );
		foreach ( var permission in new[]
		{
			"\"ClientsCanSpawnObjects\": false",
			"\"ClientsCanRefreshObjects\": false",
			"\"ClientsCanDestroyObjects\": false",
			// Host migration would hand authority to a machine with no host application,
			// no domain services, and no persistence lease; both flags must stay closed.
			"\"DestroyLobbyWhenHostLeaves\": true",
			"\"AutoSwitchToBestHost\": false"
		} ) StringAssert.Contains( networking, permission );
		StringAssert.Contains( networking, "\"UpdateRate\": 30" );

		var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
		StringAssert.Contains( runtime, "void Component.INetworkListener.OnBecameHost( Connection previousHost )" );
		StringAssert.Contains( runtime, "HEXAGON_HOST_MIGRATION_REFUSED" );
		StringAssert.Contains( runtime, "Networking.Disconnect()" );
		var collision = File.ReadAllText( Path.Combine( ProductRoot(), "ProjectSettings", "Collision.config" ) );
		StringAssert.Contains( collision, "\"b\": \"prediction\"" );
		StringAssert.Contains( collision, "\"r\": \"Ignore\"" );
	}

	[TestMethod]
	public void HostServicePublicationAndRpcShutdownAreFailClosed()
	{
		var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
		StringAssert.Contains( runtime, "OperationResult<HexHostServicesComponent> CreateHostServices()" );
		StringAssert.Contains( runtime, "HostServicePublication.RequirePublished" );
		StringAssert.Contains( runtime, "servicesObject.NetworkSpawn" );
		StringAssert.Contains( runtime, "if ( servicesObject.IsValid() ) servicesObject.Destroy()" );
		StringAssert.Contains( runtime, "if ( hostServices.Failed )" );

		var shutdown = runtime.IndexOf( "private async Task<OperationResult> ShutdownHostAsync()", StringComparison.Ordinal );
		var commandDrain = runtime.IndexOf( "_hostOperations.DrainAsync()", shutdown, StringComparison.Ordinal );
		var pairedDisconnectLoop = runtime.IndexOf( "foreach ( var disconnect in disconnects )", shutdown, StringComparison.Ordinal );
		var sessionDisconnect = runtime.IndexOf( "disconnect.Session.Disconnect()", pairedDisconnectLoop, StringComparison.Ordinal );
		var disconnect = runtime.IndexOf( "application.Disconnected", shutdown, StringComparison.Ordinal );
		var applicationDrain = runtime.IndexOf( "application.DisposeAsync", shutdown, StringComparison.Ordinal );
		var persistenceDrain = runtime.IndexOf( "persistence.ShutdownAsync", shutdown, StringComparison.Ordinal );
		var persistenceDispose = runtime.IndexOf( "persistence.DisposeAsync", shutdown, StringComparison.Ordinal );
		var quiescedEvidence = runtime.IndexOf( "application.CompleteQuiescedShutdown", shutdown, StringComparison.Ordinal );
		Assert.IsGreaterThanOrEqualTo( 0, shutdown );
		Assert.IsLessThan( disconnect, sessionDisconnect,
			"Each client session must be revoked immediately before its application disconnect callback." );
		Assert.IsLessThan( commandDrain, disconnect, "Disconnect callbacks must revoke sessions before RPC dispatch drains." );
		Assert.IsLessThan( applicationDrain, commandDrain, "RPC dispatch must finish before application disposal." );
		Assert.IsLessThan( persistenceDrain, applicationDrain, "Application disposal must finish before persistence shutdown." );
		Assert.IsLessThan( persistenceDispose, persistenceDrain, "Persistence must stop before it is disposed." );
		Assert.IsLessThan( quiescedEvidence, persistenceDispose, "Quiesced evidence must follow persistence disposal." );

		var services = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexHostServicesComponent.cs" ) );
		StringAssert.Contains( services, "runtime.TryStartHostOperation" );
		Assert.IsFalse( services.Contains( "_ = DispatchAsync", StringComparison.Ordinal ) );
	}

	[TestMethod]
	public void ClientStateSyncShellAppliesOnlyAfterScopeCaptureAndEncodeAborts()
	{
		var services = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexHostServicesComponent.cs" ) );
		var send = services.IndexOf( "public void SendClientState(", StringComparison.Ordinal );
		Assert.IsGreaterThanOrEqualTo( 0, send );
		var scopeCapture = services.IndexOf( "runtime.CaptureClientScope( recipient )", send, StringComparison.Ordinal );
		var encodeAbort = services.IndexOf( "LogSnapshotWireFailure( \"ENCODE\", \"client-state\"", send, StringComparison.Ordinal );
		var applyShell = services.IndexOf( "player.HostApplyPublicSnapshot( publicSnapshot )", send, StringComparison.Ordinal );
		var wireSend = services.IndexOf( "ReceiveClientState( scope.Value, encoded.Value )", send, StringComparison.Ordinal );
		Assert.IsGreaterThanOrEqualTo( 0, applyShell );
		Assert.IsGreaterThanOrEqualTo( 0, wireSend );
		Assert.IsLessThan( applyShell, scopeCapture,
			"The scope-capture abort must run before the replicated [Sync] shell is mutated." );
		Assert.IsLessThan( applyShell, encodeAbort,
			"The wire-encode abort must run before the replicated [Sync] shell is mutated." );
		Assert.IsLessThan( wireSend, applyShell,
			"The [Sync] shell mutation must sit immediately before the send, after every abort exit." );
	}

	[TestMethod]
	public void ClientLayerDoesNotReferenceServerAggregatesOrPersistence()
	{
		var forbidden = new[]
		{
			"Hexagon.V2.Persistence",
			"CharacterRecord",
			"InventoryRecord",
			"ItemRecord",
			"WorldItemRecord",
			"TypedPayload",
			"DocumentSnapshot",
			"IPersistenceProvider"
		};
		var violations = ProductFiles("Client")
			.Select(file => (File: file, Source: SourceWithoutComments(file)))
			.Where(value => ContainsAny(value.Source, forbidden))
			.Select(value => Relative(value.File))
			.ToArray();

		Assert.IsEmpty(violations,
			$"Client may depend only on snapshots, commands, IDs, and kernel results: {string.Join(", ", violations)}");
	}

	[TestMethod]
	public void SnapshotContractsExposeNoServerOnlyNamesOrTypes()
	{
		var forbiddenPropertyNames = new HashSet<string>(StringComparer.Ordinal)
		{
			"SteamId",
			"AccountId",
			"IsDirty",
			"SchemaState",
			"BanExpiresAt",
			"Traits",
			"RevisionToken"
		};
		var forbiddenTypeNames = new HashSet<string>(StringComparer.Ordinal)
		{
			"Hexagon.V2.Domain.CharacterRecord",
			"Hexagon.V2.Domain.InventoryRecord",
			"Hexagon.V2.Domain.ItemRecord",
			"Hexagon.V2.Domain.WorldItemRecord",
			"Hexagon.V2.Domain.TypedPayload"
		};
		var snapshotTypes = typeof(PlayerPublicSnapshot).Assembly.GetTypes()
			.Where(type => type.Namespace == "Hexagon.V2.Networking" &&
				(type.Name.EndsWith("Snapshot", StringComparison.Ordinal) || type == typeof(SnapshotValue)))
			.ToArray();
		var violations = new List<string>();
		foreach (var type in snapshotTypes)
		{
			foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
			{
				if (forbiddenPropertyNames.Contains(property.Name))
					violations.Add($"{type.Name}.{property.Name}");
				if (ContainsForbiddenType(property.PropertyType, forbiddenTypeNames))
					violations.Add($"{type.Name}.{property.Name}: {property.PropertyType.FullName}");
			}
		}

		Assert.IsNotEmpty(snapshotTypes);
		Assert.IsEmpty(violations,
			$"Snapshots expose server-only members: {string.Join(", ", violations)}");
	}

	[TestMethod]
	public void LegacyNamespacesAreReportedWithoutGatingV2()
	{
		var codeRoot = Path.Combine(ProductRoot(), "Code");
		var legacyFiles = Directory.EnumerateFiles(codeRoot, "*.cs", SearchOption.AllDirectories)
			.Where(file => !file.StartsWith(Path.Combine(codeRoot, "V2") + Path.DirectorySeparatorChar,
				StringComparison.OrdinalIgnoreCase))
			.Where(file => Regex.IsMatch(SourceWithoutComments(file), @"\bnamespace\s+Hexagon(?:\.|;)",
				RegexOptions.CultureInvariant))
			.Select(file => Path.GetRelativePath(ProductRoot(), file))
			.OrderBy(file => file, StringComparer.Ordinal)
			.ToArray();

		TestContext.WriteLine($"Legacy Hexagon namespace files (non-gating): {legacyFiles.Length}");
		foreach (var file in legacyFiles.Take(25))
			TestContext.WriteLine(file);
	}

	[TestMethod]
	public void PersistenceHandoffDefersToRecoveryAndQuarantineIsOperatorArmed()
	{
		var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
		// The scene-handoff gate awaits the previous owner (bounded) and then defers ownership and
		// integrity to the exclusive lease and WAL recovery — it must never fail-closed on the
		// predecessor's drain outcome again (that was the self-poisoning wedge).
		StringAssert.Contains( runtime, "AwaitPredecessorSettlementAsync" );
		StringAssert.Contains( runtime, "SceneHandoffPolicy.EvaluatePredecessor" );
		Assert.IsFalse( runtime.Contains( "did not drain cleanly", StringComparison.Ordinal ),
			"The barrier must not fail-closed on a predecessor drain; the lease and recovery are the authorities." );
		// Corruption recovery is operator-armed and one-shot, never automatic.
		// The arming is consumed per host-start (read into a local, then reset) so it cannot linger
		// and silently quarantine a later store.
		StringAssert.Contains( runtime, "var quarantineCorruptStore = HexagonRuntimeOverrides.QuarantineCorruptStore" );
		StringAssert.Contains( runtime, "HexagonRuntimeOverrides.QuarantineCorruptStore = false" );
	}

	[TestMethod]
	public void HostConstructionOccursOnlyAfterConfigurationAndRecoveredDomainValidation()
	{
		var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
		var configuration = runtime.IndexOf( "configuration.InitializeAsync", StringComparison.Ordinal );
		var validation = runtime.IndexOf( "new DomainInvariantValidator", StringComparison.Ordinal );
		var hostConstruction = runtime.IndexOf( "descriptor.CreateHostApplication", StringComparison.Ordinal );

		Assert.IsGreaterThanOrEqualTo( 0, configuration );
		Assert.IsLessThan( configuration, validation );
		Assert.IsLessThan( hostConstruction, configuration );
	}

	private static bool ContainsForbiddenType(Type type, IReadOnlySet<string> forbidden)
	{
		if (type.FullName is not null && forbidden.Contains(type.FullName))
			return true;
		if (type.IsArray)
			return ContainsForbiddenType(type.GetElementType()!, forbidden);
		return type.IsGenericType && type.GetGenericArguments().Any(argument => ContainsForbiddenType(argument, forbidden));
	}

	private static bool ContainsAny(string source, params string[] values) =>
		values.Any(value => source.Contains(value, StringComparison.Ordinal));

	private static bool IsAllowedSandboxLayer(string file)
	{
		var relative = Path.GetRelativePath(V2Root(), file);
		var separator = relative.IndexOfAny(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });
		var layer = separator < 0 ? relative : relative[..separator];
		return layer is "Runtime" or "Infrastructure";
	}

	private static IEnumerable<string> ProductFiles(params string[] layers)
	{
		var roots = layers.Length == 0
			? Directory.EnumerateDirectories(V2Root())
			: layers.Select(layer => Path.Combine(V2Root(), layer));
		return roots.Where(Directory.Exists)
			.SelectMany(root => Directory.EnumerateFiles(root, "*.cs", SearchOption.AllDirectories));
	}

	private static string SourceWithoutComments(string file)
	{
		var source = File.ReadAllText(file);
		return LineComments.Replace(BlockComments.Replace(source, string.Empty), string.Empty);
	}

	private static string Relative(string file) => Path.GetRelativePath(ProductRoot(), file);
	private static string V2Root() => Path.Combine(ProductRoot(), "Code", "V2");

	private static string ProductRoot()
	{
		var current = new DirectoryInfo(AppContext.BaseDirectory);
		while (current is not null)
		{
			if (Directory.Exists(Path.Combine(current.FullName, "Code", "V2")))
				return current.FullName;
			current = current.Parent;
		}

		throw new DirectoryNotFoundException("Could not locate the Hexagon product root from the test output directory.");
	}
}
using Hexagon.V2.Composition;
using Hexagon.V2.Kernel.Configuration;
using Hexagon.V2.Kernel.Persistence;
using Hexagon.V2.Kernel.Schema;
using Hexagon.V2.Persistence;
using KernelConfigDefinition = Hexagon.V2.Kernel.Configuration.ConfigDefinition<int>;

namespace Hexagon.V2.Tests.Composition;

[TestClass]
public sealed class SchemaPersistenceAdapterTests
{
	[TestMethod]
	public void BindValidatesAndRegistersSchemaCodecAndTypedConfigs()
	{
		var compiled = CompileSchema(2);
		var registry = new PersistedTypeRegistry();
		var codec = new JsonPersistedTypeCodec<Payload>(new PersistedTypeKey("schema.payload"), 2,
			PersistedValuePublication.Immutable,
			upgrades: new Dictionary<int, Func<System.Text.Json.JsonElement, System.Text.Json.JsonElement>>
			{
				[1] = value => value
			});

		var result = SchemaPersistenceAdapter.Bind(compiled, registry, new[] { codec });

		Assert.IsTrue(result.Succeeded, result.Error?.Message);
		Assert.AreSame(registry, result.Value.Types);
		Assert.AreEqual(typeof(Payload), registry.Resolve(new PersistedTypeKey("schema.payload")).ClrType);
		Assert.AreEqual(4, result.Value.Configs.Require<int>("slots").Value.DefaultValue);
		Assert.HasCount(1, result.Value.PersistenceConfigs);
		var persistedConfig = result.Value.PersistenceConfigs["slots"];
		var encodedConfig = persistedConfig.SerializeObject(7);
		Assert.AreEqual(7, persistedConfig.DeserializeObject(encodedConfig));
	}

	[TestMethod]
	public void VersionMismatchFailsBeforeMutatingRegistry()
	{
		var compiled = CompileSchema(2);
		var registry = new PersistedTypeRegistry();
		var codec = new JsonPersistedTypeCodec<Payload>(new PersistedTypeKey("schema.payload"), 1,
			PersistedValuePublication.Immutable);

		var result = SchemaPersistenceAdapter.Bind(compiled, registry, new[] { codec });

		Assert.IsTrue(result.Failed);
		Assert.IsEmpty(registry.Codecs);
		StringAssert.Contains(result.Error!.Message, "does not match schema version");
	}

	[TestMethod]
	public void MissingAndUnknownCodecsFailClosed()
	{
		var compiled = CompileSchema(1);
		var missingRegistry = new PersistedTypeRegistry();
		var unknownRegistry = new PersistedTypeRegistry();

		var missing = SchemaPersistenceAdapter.Bind(compiled, missingRegistry,
			Array.Empty<IPersistedTypeCodec>());
		var unknown = SchemaPersistenceAdapter.Bind(compiled, unknownRegistry,
			new IPersistedTypeCodec[]
			{
				new JsonPersistedTypeCodec<OtherPayload>(new PersistedTypeKey("schema.other"), 1,
					PersistedValuePublication.Immutable)
			});

		Assert.IsTrue(missing.Failed);
		Assert.IsTrue(unknown.Failed);
		Assert.IsEmpty(missingRegistry.Codecs);
		Assert.IsEmpty(unknownRegistry.Codecs);
	}

	[TestMethod]
	public void ExistingCollisionFailsBeforeAddingAnySchemaCodec()
	{
		var compiled = CompileSchema(1);
		var registry = new PersistedTypeRegistry()
			.Register<OtherPayload>(new PersistedTypeKey("schema.payload"), 1,
				PersistedValuePublication.Immutable);
		var codec = new JsonPersistedTypeCodec<Payload>(new PersistedTypeKey("schema.payload"), 1,
			PersistedValuePublication.Immutable);

		var result = SchemaPersistenceAdapter.Bind(compiled, registry, new[] { codec });

		Assert.IsTrue(result.Failed);
		Assert.HasCount(1, registry.Codecs);
		Assert.AreEqual(typeof(OtherPayload), registry.Codecs.Single().ClrType);
	}

	[TestMethod]
	public void ConfigurationWithoutStablePersistenceTypeFailsBinding()
	{
		var compiled = SchemaCompiler.Compile( new UnsupportedConfigurationSchema() );
		Assert.IsTrue( compiled.Succeeded, compiled.Error?.Message );
		var result = SchemaPersistenceAdapter.Bind(
			compiled.Value,
			new PersistedTypeRegistry(),
			Array.Empty<IPersistedTypeCodec>() );

		Assert.IsTrue( result.Failed );
		Assert.AreEqual( Hexagon.V2.Kernel.ErrorCode.ConfigurationInvalid, result.Error!.Code );
		StringAssert.Contains( result.Error.Message, "stable persistence identity" );
	}

	private static CompiledSchema CompileSchema(int version)
	{
		var result = SchemaCompiler.Compile(new TestSchema(version));
		Assert.IsTrue(result.Succeeded, result.Error?.Message);
		return result.Value;
	}

	private sealed class TestSchema : IHexSchema
	{
		private readonly int _version;
		public TestSchema(int version) => _version = version;
		public string Id => "test_schema";

		public void Configure(SchemaBuilder builder)
		{
			builder.RegisterPersistedType(
				new PersistedTypeRegistration("schema.payload", typeof(Payload), _version));
			builder.RegisterConfig(new KernelConfigDefinition("slots", 4, ConfigCodecs.Int32));
		}
	}

	private sealed record Payload(string Value);
	private sealed record OtherPayload(string Value);

	private sealed class UnsupportedConfigurationSchema : IHexSchema
	{
		public string Id => "unsupported_config";

		public void Configure( SchemaBuilder builder ) => builder.RegisterConfig(
			new Hexagon.V2.Kernel.Configuration.ConfigDefinition<DateTimeOffset>(
				"unsupported",
				DateTimeOffset.UnixEpoch,
				new UnsupportedConfigurationCodec() ) );
	}

	private sealed class UnsupportedConfigurationCodec : IConfigCodec<DateTimeOffset>
	{
		public Hexagon.V2.Kernel.OperationResult<string> Encode( DateTimeOffset value ) =>
			Hexagon.V2.Kernel.OperationResult<string>.Success( value.ToString( "O" ) );

		public Hexagon.V2.Kernel.OperationResult<DateTimeOffset> Decode( string encoded ) =>
			Hexagon.V2.Kernel.OperationResult<DateTimeOffset>.Success( DateTimeOffset.Parse( encoded ) );
	}
}
using System.Threading;
using System.Threading.Tasks;
using Hexagon.V2.Kernel;

namespace Hexagon.V2.Tests.Kernel;

[TestClass]
public sealed class AsyncOperationCaptureTests
{
	[TestMethod]
	public async Task SynchronousThrowIsCapturedAsAFailureOutcome()
	{
		var thrown = new InvalidOperationException( "boom" );
		var outcome = await AsyncOperation.Capture( () => throw thrown );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public async Task SynchronousThrowIsCapturedAsAFailureOutcomeWithValue()
	{
		var thrown = new InvalidOperationException( "boom" );
		var outcome = await AsyncOperation.Capture<int>( () => throw thrown );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public void CompletedOperationTakesTheSynchronousFastPath()
	{
		var task = AsyncOperation.Capture( () => ValueTask.CompletedTask );

		Assert.IsTrue( task.IsCompletedSuccessfully, "A completed ValueTask must not allocate a continuation." );
		Assert.IsTrue( task.Result.Succeeded );
	}

	[TestMethod]
	public void CompletedOperationWithValueTakesTheSynchronousFastPath()
	{
		var task = AsyncOperation.Capture( () => ValueTask.FromResult( 42 ) );

		Assert.IsTrue( task.IsCompletedSuccessfully, "A completed ValueTask must not allocate a continuation." );
		Assert.IsTrue( task.Result.Succeeded );
		Assert.AreEqual( 42, task.Result.Value );
	}

	[TestMethod]
	public async Task AsynchronousFaultIsUnwrappedFromItsAggregateException()
	{
		var thrown = new InvalidOperationException( "inner" );
		var outcome = await AsyncOperation.Capture(
			() => new ValueTask( Task.FromException( thrown ) ) );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public async Task AsynchronousFaultWithValueIsUnwrappedFromItsAggregateException()
	{
		var thrown = new InvalidOperationException( "inner" );
		var outcome = await AsyncOperation.Capture(
			() => new ValueTask<int>( Task.FromException<int>( thrown ) ) );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public async Task ThrowAfterAnAwaitPointIsCaptured()
	{
		var thrown = new InvalidOperationException( "late" );
		var outcome = await AsyncOperation.Capture( async () =>
		{
			await Task.Yield();
			throw thrown;
		} );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public async Task CancellationBecomesAFailureOutcomeInsteadOfAThrow()
	{
		var canceled = new CancellationToken( canceled: true );
		var outcome = await AsyncOperation.Capture(
			() => new ValueTask( Task.FromCanceled( canceled ) ) );

		Assert.IsFalse( outcome.Succeeded );
		Assert.IsInstanceOfType<OperationCanceledException>( outcome.Exception );
	}

	[TestMethod]
	public async Task CancellationWithValueBecomesAFailureOutcomeInsteadOfAThrow()
	{
		var canceled = new CancellationToken( canceled: true );
		var outcome = await AsyncOperation.Capture(
			() => new ValueTask<int>( Task.FromCanceled<int>( canceled ) ) );

		Assert.IsFalse( outcome.Succeeded );
		Assert.IsInstanceOfType<OperationCanceledException>( outcome.Exception );
	}

	[TestMethod]
	public void CaptureSynchronousConvertsAThrowingActionIntoAFailure()
	{
		var thrown = new InvalidOperationException( "boom" );
		var outcome = AsyncOperation.CaptureSynchronous( () => throw thrown );

		Assert.IsFalse( outcome.Succeeded );
		Assert.AreSame( thrown, outcome.Exception );
	}

	[TestMethod]
	public void CaptureSynchronousReturnsTheProducedValueOnSuccess()
	{
		var outcome = AsyncOperation.CaptureSynchronous( () => 42 );

		Assert.IsTrue( outcome.Succeeded );
		Assert.AreEqual( 42, outcome.Value );
	}
}
#nullable enable

using Hexagon.V2.Domain;
using Hexagon.V2.Networking;

namespace Hexagon.V2.Tests.Networking;

[TestClass]
public sealed class ConnectionSessionBoundaryTests
{
	[TestMethod]
	public void CharacterTransitionCancelsStableLeaseButNotConnectionLease()
	{
		using var boundary = new ConnectionSessionBoundary( ConnectionEpoch.New() );
		var firstCharacter = CharacterId.New();
		var stable = boundary.Capture( firstCharacter, true );
		var connection = boundary.Capture( firstCharacter, false );

		boundary.ObserveCharacter( CharacterId.New() );

		Assert.IsTrue( stable.CancellationToken.IsCancellationRequested );
		Assert.IsFalse( connection.CancellationToken.IsCancellationRequested );
		Assert.IsFalse( boundary.IsCurrent( stable ) );
		Assert.IsTrue( boundary.IsCurrent( connection ) );
	}

	[TestMethod]
	public void DisconnectCancelsEveryLeaseAndRejectsCurrentChecks()
	{
		using var boundary = new ConnectionSessionBoundary( ConnectionEpoch.New() );
		var stable = boundary.Capture( CharacterId.New(), true );
		var connection = boundary.Capture( boundary.CharacterId, false );

		boundary.Disconnect();

		Assert.IsTrue( stable.CancellationToken.IsCancellationRequested );
		Assert.IsTrue( connection.CancellationToken.IsCancellationRequested );
		Assert.IsFalse( boundary.IsCurrent( stable ) );
		Assert.IsFalse( boundary.IsCurrent( connection ) );
	}

	[TestMethod]
	public void PublishedEpochOrdersStateAndOnlyAdvancesCharacterOnIdentityChange()
	{
		using var boundary = new ConnectionSessionBoundary( ConnectionEpoch.New() );
		var character = CharacterId.New();

		var first = boundary.Publish( null );
		var second = boundary.Publish( character );
		var third = boundary.Publish( character );
		var fourth = boundary.Publish( null );

		Assert.AreEqual( 0L, first.Character );
		Assert.AreEqual( 1L, second.Character );
		Assert.AreEqual( 1L, third.Character );
		Assert.AreEqual( 2L, fourth.Character );
		Assert.AreEqual( 1L, first.Revision );
		Assert.AreEqual( 4L, fourth.Revision );
		Assert.AreEqual( first.Connection, fourth.Connection );
	}

	[TestMethod]
	public void ThrowingCancellationCallbackCannotEscapeDisconnectBoundary()
	{
		var diagnostics = new List<Exception>();
		using var boundary = new ConnectionSessionBoundary( ConnectionEpoch.New(), diagnostics.Add );
		var lease = boundary.Capture( CharacterId.New(), true );
		using var registration = lease.CancellationToken.Register( () => throw new InvalidOperationException( "callback" ) );

		boundary.Disconnect();

		Assert.IsTrue( lease.CancellationToken.IsCancellationRequested );
		Assert.HasCount( 1, diagnostics );
	}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	[AssemblyInitialize]
	public static void ClassInitialize( TestContext context )
	{
		Sandbox.Application.InitUnitTest();
	}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
#nullable enable

using System.Text.Json;
using Hexagon.V2.Application;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Kernel.Definitions;
using Hexagon.V2.Kernel.Events;
using Hexagon.V2.Kernel.Persistence;
using Hexagon.V2.Kernel.Policies;
using Hexagon.V2.Kernel.Schema;
using Hexagon.V2.Persistence;
using KernelClassDefinition = Hexagon.V2.Kernel.Definitions.ClassDefinition;
using KernelFactionDefinition = Hexagon.V2.Kernel.Definitions.FactionDefinition;
using KernelItemDefinition = Hexagon.V2.Kernel.Definitions.ItemDefinition;

namespace Hexagon.V2.Tests.Application;

internal sealed class ApplicationServiceTestEnvironment : IAsyncDisposable
{
	private readonly HashSet<ConnectionId> _openConnections = new();
	public const string StateTypeId = "test.character-state";
	public const string ItemDefinitionId = "test.item";
	public const string BagDefinitionId = "test.bag";
	public const string WorldModel = "models/test/item.vmdl";

	private ApplicationServiceTestEnvironment(
		FaultInjectingPersistenceProvider provider,
		CompiledSchema schema,
		DomainRepositories repositories)
	{
		Provider = provider;
		Schema = schema;
		Repositories = repositories;
		PersistenceProfile = new SchemaPersistenceInvariantProfile(
			new PersistedTypeId( StateTypeId ),
			new[]
			{
				ItemPersistenceContract.WithoutTraits( ItemDefinitionId ),
				ItemPersistenceContract.WithoutTraits( BagDefinitionId )
			},
			Array.Empty<KeyValuePair<string, PersistedTypeId>>(),
			Array.Empty<KeyValuePair<string, PersistedTypeId>>() );
		Access = new InventoryAccessService();
		Layout = new InventoryLayoutService(new SchemaItemShapeCatalog(schema, repositories));
	}

	public FaultInjectingPersistenceProvider Provider { get; }
	public CompiledSchema Schema { get; }
	public DomainRepositories Repositories { get; }
	public SchemaPersistenceInvariantProfile PersistenceProfile { get; }
	public InventoryAccessService Access { get; }
	public InventoryLayoutService Layout { get; }

	public static async Task<ApplicationServiceTestEnvironment> CreateAsync(int? classCapacity = null)
	{
		var types = new PersistedTypeRegistry()
			.RegisterHexagonDomainTypes()
			.Register<TestCharacterState>(new PersistedTypeKey(StateTypeId), 1,
				PersistedValuePublication.Immutable);
		var provider = new FaultInjectingPersistenceProvider(new InMemoryPersistenceProvider(types));
		await provider.InitializeAsync();

		var compiled = SchemaCompiler.Compile(new TestSchema(classCapacity));
		if (compiled.Failed)
			throw new InvalidOperationException(compiled.Error!.Message);

		return new ApplicationServiceTestEnvironment(
			provider,
			compiled.Value,
			new DomainRepositories(provider));
	}

	public CharacterService CreateCharacterService(
		IEnumerable<ICharacterInitializer>? initializers = null,
		int inventoryWidth = 4,
		int inventoryHeight = 4,
		ICharacterStateFactory? stateFactory = null)
	{
		return new CharacterService(
			Repositories,
			Schema,
			new AllowAllModels(),
			stateFactory ?? new TestStateFactory(),
			initializers ?? Array.Empty<ICharacterInitializer>(),
			new TestIdGenerator(),
			new FixedClock(),
			Layout,
			AllowPolicy<CharacterCreationContext>(),
			AllowPolicy<CharacterDeletionContext>(),
			inventoryWidth: inventoryWidth,
			inventoryHeight: inventoryHeight);
	}

	public InventoryMutationService CreateInventoryMutationService() => new(
		Repositories,
		Access,
		Layout,
		AllowPolicy<InventoryTransferContext>());

	public WorldItemService CreateWorldItemService(bool modelIsValid = true) => new(
		Repositories,
		Schema,
		Access,
		Layout,
		new TestWorldModelCatalog(modelIsValid),
		AllowPolicy<WorldDropContext>(),
		AllowPolicy<WorldPickupContext>());

	public async Task SeedAsync(Action<IUnitOfWork> stage)
	{
		await using var unitOfWork = Provider.BeginUnitOfWork();
		stage(unitOfWork);
		var committed = await unitOfWork.CommitAsync();
		if (!committed.Succeeded)
			throw new InvalidOperationException(committed.Error!.Message);
	}

	public void Grant(
		InventoryActor actor,
		InventoryId inventoryId,
		InventoryCapability capabilities)
	{
		OpenConnection(actor.ConnectionId);
		Access.Grant(new InventoryGrant
		{
			ConnectionId = actor.ConnectionId,
			CharacterId = actor.CharacterId,
			InventoryId = inventoryId,
			Capabilities = capabilities,
			Kind = InventoryGrantKind.Character
		});
	}

	public void OpenConnection(ConnectionId connectionId)
	{
		if (_openConnections.Add(connectionId)) Access.OpenConnection(connectionId);
	}

	public static InventoryActor Actor() => new(ConnectionId.New(), new AccountId(101), CharacterId.New());

	public static CharacterCreationRequest Request(
		IReadOnlyDictionary<string, CreationValue>? fields = null) => new()
	{
		Name = "  Alyx Vance  ",
		Description = "  A sufficiently detailed character description.  ",
		Model = new DefinitionId("citizen_model"),
		Faction = new FactionId("citizen"),
		Fields = fields ?? new Dictionary<string, CreationValue>(StringComparer.Ordinal)
		{
			["nickname"] = CreationValue.String("alyx")
		}
	};

	public static CharacterRecord Character(AccountId account, int slot, CharacterId? id = null) => new()
	{
		Id = id ?? CharacterId.New(),
		AccountId = account,
		Slot = slot,
		Name = $"Character {slot}",
		Description = "A sufficiently detailed seeded description.",
		Model = new DefinitionId("citizen_model"),
		Faction = new FactionId("citizen"),
		Class = new ClassId("worker"),
		Balance = 0,
		CreatedAt = DateTimeOffset.UnixEpoch,
		LastPlayedAt = DateTimeOffset.UnixEpoch,
		SchemaState = StatePayload()
	};

	public static InventoryRecord Inventory(
		InventoryOwner owner,
		IEnumerable<InventoryPlacement>? placements = null,
		int width = 4,
		int height = 4,
		InventoryId? id = null) => new()
	{
		Id = id ?? InventoryId.New(),
		Owner = owner,
		Width = width,
		Height = height,
		Placements = placements?.ToArray() ?? Array.Empty<InventoryPlacement>()
	};

	public static ItemRecord Item(string definition = ItemDefinitionId, ItemId? id = null) => new()
	{
		Id = id ?? ItemId.New(),
		Definition = new DefinitionId(definition)
	};

	/// <summary>
	/// Canonical owner index record for an inventory, matching the commit-time invariant
	/// production enforces (one record per inventory under the owner-kind's canonical role).
	/// </summary>
	public static OwnerInventoryRecord OwnerIndex(InventoryRecord inventory, string role) => new()
	{
		Role = role,
		Owner = inventory.Owner,
		InventoryId = inventory.Id
	};

	public static WorldTransformRecord Transform() => new()
	{
		PositionX = 10,
		PositionY = 20,
		PositionZ = 30,
		RotationX = 0,
		RotationY = 0,
		RotationZ = 0,
		RotationW = 1
	};

	public static TypedPayload StatePayload(
		string name = "citizen",
		string typeId = StateTypeId,
		int version = 1) => new()
	{
		TypeId = new PersistedTypeId(typeId),
		TypeVersion = version,
		Data = JsonSerializer.SerializeToElement(
			new TestCharacterState(name),
			new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })
	};

	public static PolicyPipeline<TContext> AllowPolicy<TContext>() => new(
		new PolicyHandler<TContext>("built_in", new AllowPolicyHandler<TContext>()));

	public ValueTask DisposeAsync() => Provider.DisposeAsync();

	private sealed class TestSchema : IHexSchema
	{
		private readonly int? _classCapacity;
		public TestSchema(int? classCapacity) => _classCapacity = classCapacity;
		public string Id => "test_schema";

		public void Configure(SchemaBuilder builder)
		{
			builder.RegisterCharacterField(new CharacterFieldDefinition(
				"nickname", CharacterFieldValueKind.String, true, true));
			builder.RegisterFaction(new KernelFactionDefinition("citizen", true, "worker", "Citizen"));
			builder.RegisterClass(new KernelClassDefinition("worker", "citizen", "Worker", _classCapacity));
			builder.RegisterItem(new KernelItemDefinition(
				ItemDefinitionId,
				Array.Empty<string>(),
				true,
				WorldModel,
				"Test Item"));
			builder.RegisterItem(new KernelItemDefinition(
				BagDefinitionId,
				Array.Empty<string>(),
				true,
				WorldModel,
				"Test Bag"));
			builder.RegisterPersistedType(new PersistedTypeRegistration(
				StateTypeId, typeof(TestCharacterState), 1));
		}
	}

	private sealed class AllowAllModels : ICharacterModelCatalog
	{
		public bool IsAllowed(DefinitionId model, FactionId faction, ClassId? characterClass) => true;
	}

	private sealed class TestStateFactory : ICharacterStateFactory
	{
		public OperationResult<CharacterStatePlan> Create(CharacterCreationContext context) =>
			OperationResult<CharacterStatePlan>.Success(new CharacterStatePlan(StatePayload(), 25));
	}

	private sealed class FixedClock : IHexClock
	{
		public DateTimeOffset UtcNow => new(2030, 1, 2, 3, 4, 5, TimeSpan.Zero);
	}

	private sealed class TestIdGenerator : IAggregateIdGenerator
	{
		public CharacterId NewCharacterId() => CharacterId.New();
		public InventoryId NewInventoryId() => InventoryId.New();
		public ItemId NewItemId() => ItemId.New();
		public InteractionSessionId NewInteractionSessionId() => InteractionSessionId.New();
	}

	private sealed class TestWorldModelCatalog : IWorldModelCatalog
	{
		private readonly bool _isValid;
		public TestWorldModelCatalog(bool isValid) => _isValid = isValid;
		public bool IsValidModel(string modelPath) => _isValid && modelPath == WorldModel;
	}

	private sealed class AllowPolicyHandler<TContext> : IPolicy<TContext>
	{
		public PolicyDecision Evaluate(TContext context) => PolicyDecision.Allow();
	}
}

internal sealed record TestCharacterState(string Name);

internal sealed class TestCharacterInitializer : ICharacterInitializer
{
	private readonly Func<CharacterCreationContext, CharacterStatePlan,
		OperationResult<CharacterInitializerContribution>> _build;

	public TestCharacterInitializer(
		string id,
		Func<CharacterCreationContext, CharacterStatePlan,
			OperationResult<CharacterInitializerContribution>> build,
		int order = 0)
	{
		Id = id;
		Order = order;
		_build = build;
	}

	public string Id { get; }
	public int Order { get; }
	public OperationResult<CharacterInitializerContribution> Build(
		CharacterCreationContext context,
		CharacterStatePlan state) => _build(context, state);
}

internal sealed class FaultInjectingPersistenceProvider : IPersistenceProvider
{
	private readonly IPersistenceProvider _inner;
	private readonly Dictionary<string, int> _allCalls = new(StringComparer.Ordinal);
	private bool _failNextCommit;
	private Action? _beforeNextCommit;

	public FaultInjectingPersistenceProvider(IPersistenceProvider inner) =>
		_inner = inner ?? throw new ArgumentNullException(nameof(inner));

	public PersistedTypeRegistry Types => _inner.Types;
	public PersistenceHealth Health => _inner.Health;
	public bool IsInitialized => _inner.IsInitialized;
	public PersistenceProviderState State => _inner.State;
	public Guid StoreId => _inner.StoreId;
	public Guid WriterEpoch => _inner.WriterEpoch;
	public long CompactionGeneration => _inner.CompactionGeneration;

	public void FailNextCommit() => _failNextCommit = true;
	public void BeforeNextCommit(Action callback)
	{
		ArgumentNullException.ThrowIfNull(callback);
		if (Interlocked.CompareExchange(ref _beforeNextCommit, callback, null) is not null)
			throw new InvalidOperationException("A before-commit callback is already pending.");
	}

	public ValueTask InitializeAsync(CancellationToken cancellationToken = default) =>
		_inner.InitializeAsync(cancellationToken);

	public IPersistenceRepository<T> Repository<T>(string collection) where T : class =>
		new CountingRepository<T>(this, _inner.Repository<T>(collection));

	/// <summary>
	/// Number of full-collection All() enumerations issued so far, for asserting that
	/// keyed-probe code paths never fall back to store scans.
	/// </summary>
	public int AllCallCount(string collection)
	{
		lock (_allCalls) return _allCalls.GetValueOrDefault(collection);
	}

	private void RecordAllCall(string collection)
	{
		lock (_allCalls) _allCalls[collection] = _allCalls.GetValueOrDefault(collection) + 1;
	}

	public IUnitOfWork BeginUnitOfWork() => new FaultInjectingUnitOfWork(this, _inner.BeginUnitOfWork());

	public ValueTask<PersistenceResult<long>> CheckpointAsync(CancellationToken cancellationToken = default) =>
		_inner.CheckpointAsync(cancellationToken);

	public ValueTask<PersistenceShutdownResult> ShutdownAsync(CancellationToken cancellationToken = default) =>
		_inner.ShutdownAsync(cancellationToken);

	public ValueTask DisposeAsync() => _inner.DisposeAsync();

	private bool ConsumeCommitFailure()
	{
		if (!_failNextCommit)
			return false;

		_failNextCommit = false;
		return true;
	}

	private Action? ConsumeBeforeCommit() => Interlocked.Exchange(ref _beforeNextCommit, null);

	private static IPersistenceRepository<T> Unwrap<T>(IPersistenceRepository<T> repository) where T : class =>
		repository is CountingRepository<T> counting ? counting.Inner : repository;

	private sealed class CountingRepository<T> : IPersistenceRepository<T> where T : class
	{
		private readonly FaultInjectingPersistenceProvider _provider;

		public CountingRepository(FaultInjectingPersistenceProvider provider, IPersistenceRepository<T> inner)
		{
			_provider = provider;
			Inner = inner;
		}

		public IPersistenceRepository<T> Inner { get; }
		public string Collection => Inner.Collection;
		public DocumentSnapshot<T>? Find(string key) => Inner.Find(key);

		public IReadOnlyList<DocumentSnapshot<T>> All()
		{
			_provider.RecordAllCall(Inner.Collection);
			return Inner.All();
		}
	}

	private sealed class FaultInjectingUnitOfWork : IUnitOfWork
	{
		private readonly FaultInjectingPersistenceProvider _provider;
		private readonly IUnitOfWork _inner;

		public FaultInjectingUnitOfWork(
			FaultInjectingPersistenceProvider provider,
			IUnitOfWork inner)
		{
			_provider = provider;
			_inner = inner;
		}

		public DocumentEditor<T>? Edit<T>(IPersistenceRepository<T> repository, DocumentSnapshot<T> observed) where T : class =>
			_inner.Edit(Unwrap(repository), observed);

		public void RequireUnchanged<T>(IPersistenceRepository<T> repository, DocumentSnapshot<T> observed) where T : class =>
			_inner.RequireUnchanged(Unwrap(repository), observed);

		public void Create<T>(IPersistenceRepository<T> repository, string key, T value) where T : class =>
			_inner.Create(Unwrap(repository), key, value);

		public void Put<T>(IPersistenceRepository<T> repository, string key, T value) where T : class =>
			_inner.Put(Unwrap(repository), key, value);

		public void Save<T>(DocumentEditor<T> editor) where T : class => _inner.Save(editor);

		public void Delete<T>(IPersistenceRepository<T> repository, DocumentSnapshot<T> observed) where T : class =>
			_inner.Delete(Unwrap(repository), observed);

		public void Require(ICommitPrecondition precondition) => _inner.Require(precondition);

		public ValueTask<PersistenceResult<CommitReceipt>> CommitAsync(
			CancellationToken cancellationToken = default)
		{
			_provider.ConsumeBeforeCommit()?.Invoke();
			if (!_provider.ConsumeCommitFailure())
				return _inner.CommitAsync(cancellationToken);

			return ValueTask.FromResult(PersistenceResult<CommitReceipt>.Failure(
				new PersistenceError(
					PersistenceErrorCode.DurabilityFailed,
					"Injected commit failure.")));
		}

		public ValueTask DisposeAsync() => _inner.DisposeAsync();
	}
}
#nullable enable

using System;
using System.Collections.Generic;
using Hexagon.V2.Client;
using Hexagon.V2.Domain;
using Hexagon.V2.Networking;

namespace Hexagon.V2.Tests.Client;

[TestClass]
public sealed class HexClientStoreTests
{
	[TestMethod]
	public void ThrowingSubscriberDoesNotSkipLaterSubscribersOrEscapeThePublish()
	{
		var store = new HexClientStore();
		var diagnostics = new List<Exception>();
		store.SubscriberFailureDiagnostic = diagnostics.Add;
		var received = new List<ClientStoreChange>();
		store.Changed += _ => throw new InvalidOperationException( "first subscriber threw" );
		store.Changed += received.Add;

		store.PrepareSession( ClientSessionNonce.New() );

		Assert.HasCount( 1, received, "The second subscriber must still observe the change." );
		Assert.AreEqual( ClientStoreChangeKind.SessionStarted, received[0].Kind );
		Assert.AreEqual( 1, store.SubscriberFailureCount );
		Assert.HasCount( 1, diagnostics );
		Assert.IsInstanceOfType<InvalidOperationException>( diagnostics[0] );
	}

	[TestMethod]
	public void CompleteStatePublicationAtomicallyTransitionsAndUnloadsCharacter()
	{
		var store = new HexClientStore();
		var changes = new List<ClientStoreChange>();
		store.Changed += changes.Add;
		var connection = ConnectionEpoch.New();
		var characterId = CharacterId.New();
		var characterList = CharacterList( characterId );
		var chat = Chat( new ChatDeliveryEpoch( connection, 1 ) );

		var scope = Establish( store, connection );
		store.ReplaceCharacterList( scope, characterList );
		Assert.IsTrue( store.ApplyState( scope, State( connection, 1, 1, characterId, new[] { Inventory() }, Action() ) ) );
		store.ReplaceChat( scope, chat );

		Assert.AreEqual( ClientLifecycleState.CharacterActive, store.Lifecycle );
		Assert.IsNotNull( store.PublicPlayer );
		Assert.IsNotNull( store.PrivatePlayer );
		Assert.HasCount( 1, store.Inventories );
		Assert.IsNotNull( store.ActiveAction );
		Assert.AreSame( chat, store.Chat );

		Assert.IsTrue( store.ApplyState( scope, State( connection, 2, 2, null ) ) );

		Assert.AreEqual( ClientLifecycleState.Connected, store.Lifecycle );
		Assert.IsNotNull( store.PublicPlayer );
		Assert.IsNull( store.PublicPlayer.CharacterId );
		Assert.IsNull( store.PrivatePlayer );
		Assert.IsEmpty( store.Inventories );
		Assert.IsNull( store.ActiveAction );
		Assert.AreSame( characterList, store.CharacterList );
		Assert.IsNull( store.Chat );
		Assert.AreEqual( ClientStoreChangeKind.StateApplied, changes[^1].Kind );

		store.ClearSession();
		Assert.AreEqual( ClientLifecycleState.Disconnected, store.Lifecycle );
		Assert.IsNull( store.State );
		Assert.IsNull( store.CharacterList );
		Assert.IsNull( store.Chat );
	}

	[TestMethod]
	public void CharacterSwitchReplacesEveryCharacterScopedViewInOnePublication()
	{
		var store = new HexClientStore();
		var connection = ConnectionEpoch.New();
		var scope = Establish( store, connection );
		var first = CharacterId.New();
		var second = CharacterId.New();
		store.ApplyState( scope, State( connection, 1, 1, first, new[] { Inventory( "First" ) }, Action() ) );
		store.ReplaceChat( scope, Chat( new ChatDeliveryEpoch( connection, 1 ) ) );

		var secondInventory = Inventory( "Second" );
		Assert.IsTrue( store.ApplyState( scope, State( connection, 2, 2, second, new[] { secondInventory } ) ) );

		Assert.AreEqual( second, store.PublicPlayer!.CharacterId );
		Assert.AreEqual( second, store.PrivatePlayer!.CharacterId );
		Assert.HasCount( 1, store.Inventories );
		Assert.AreEqual( "Second", store.Inventories[0].Title );
		Assert.IsNull( store.ActiveAction );
		Assert.IsNull( store.Chat, "Character-epoch changes must clear receiver-scoped chat history." );
	}

	[TestMethod]
	public void LateRevisionWrongConnectionAndUnversionedCharacterSwitchAreRejected()
	{
		var store = new HexClientStore();
		var firstConnection = ConnectionEpoch.New();
		var scope = Establish( store, firstConnection );
		var character = CharacterId.New();
		var accepted = State( firstConnection, 4, 10, character, new[] { Inventory( "Accepted" ) } );
		Assert.IsTrue( store.ApplyState( scope, accepted ) );

		Assert.IsFalse( store.ApplyState( scope, State( firstConnection, 4, 9, character ) ) );
		Assert.IsFalse( store.ApplyState( scope, State( ConnectionEpoch.New(), 5, 11, CharacterId.New() ) ) );
		Assert.IsFalse( store.ApplyState( scope, State( firstConnection, 4, 11, CharacterId.New() ) ) );

		Assert.AreSame( accepted, store.State );
		Assert.AreEqual( "Accepted", store.Inventories[0].Title );
	}

	[TestMethod]
	public void SnapshotRejectsMismatchedPrivateCharacterAndDuplicateInventories()
	{
		var connection = ConnectionEpoch.New();
		var character = CharacterId.New();
		Assert.ThrowsExactly<ArgumentException>( () => new ClientStateSnapshot(
			new ClientStateEpoch( connection, 1, 1 ),
			Public( character ),
			Private( CharacterId.New() ),
			PlayerRosterSnapshot.Empty,
			null,
			Array.Empty<InventorySnapshot>(),
			null ) );

		var inventory = Inventory();
		Assert.ThrowsExactly<ArgumentException>( () => new ClientStateSnapshot(
			new ClientStateEpoch( connection, 1, 1 ),
			Public( character ),
			Private( character ),
			PlayerRosterSnapshot.Empty,
			null,
			new[] { inventory, Inventory( "Duplicate", inventory.InventoryId ) },
			null ) );
	}

	[TestMethod]
	public void RosterAndSchemaViewsAreImmutableAtomicAndProtectedByTheSameEpoch()
	{
		var connection = ConnectionEpoch.New();
		var character = CharacterId.New();
		var rosterFields = new Dictionary<string, SnapshotValue>( StringComparer.Ordinal )
		{
			["name"] = SnapshotValue.String( "Alyx" )
		};
		var rosterRows = new List<PlayerRosterRowSnapshot>
		{
			new( ConnectionId.New(), character, rosterFields )
		};
		var roster = new PlayerRosterSnapshot( 4, rosterRows );
		var panelFields = new Dictionary<string, SnapshotValue>( StringComparer.Ordinal )
		{
			["title"] = SnapshotValue.String( "Permit Vendor" )
		};
		var row = new Dictionary<string, SnapshotValue>( StringComparer.Ordinal )
		{
			["stock"] = SnapshotValue.Integer( 3 )
		};
		var schemaView = new SchemaViewSnapshot( "vendor", 8, panelFields, new[] { row } );
		var accepted = State(
			connection, 1, 1, character,
			roster: roster,
			schemaViews: new[] { schemaView } );

		rosterFields["name"] = SnapshotValue.String( "Mutated" );
		rosterRows.Clear();
		panelFields["title"] = SnapshotValue.String( "Mutated" );
		row["stock"] = SnapshotValue.Integer( 0 );
		var store = new HexClientStore();
		var scope = Establish( store, connection );
		Assert.IsTrue( store.ApplyState( scope, accepted ) );

		Assert.AreEqual( "Alyx", store.Roster!.Rows[0].Fields["name"].StringValue );
		Assert.AreEqual( "Permit Vendor", store.SchemaViews!["vendor"].Fields["title"].StringValue );
		Assert.AreEqual( 3L, store.SchemaViews["vendor"].Rows[0]["stock"].IntegerValue );

		var lateRoster = new PlayerRosterSnapshot( 5 );
		Assert.IsFalse( store.ApplyState( scope, State(
			connection, 1, 1, character,
			roster: lateRoster,
			schemaViews: Array.Empty<SchemaViewSnapshot>() ) ) );
		Assert.AreSame( roster, store.Roster );
		Assert.IsTrue( store.SchemaViews.ContainsKey( "vendor" ) );
	}

	[TestMethod]
	public void AtomicStateRejectsDuplicatePanelIds()
	{
		var character = CharacterId.New();
		var view = new SchemaViewSnapshot( "civic", 1 );
		Assert.ThrowsExactly<ArgumentException>( () => State(
			ConnectionEpoch.New(), 1, 1, character,
			roster: PlayerRosterSnapshot.Empty,
			schemaViews: new[] { view, new SchemaViewSnapshot( "civic", 2 ) } ) );
	}

	[TestMethod]
	public void BeginSessionIsInstanceScopedAndIndependentOfListenHostState()
	{
		var hostProcessStore = new HexClientStore();
		var clientScopeStore = new HexClientStore();
		var hostCharacter = CharacterId.New();
		var hostConnection = ConnectionEpoch.New();
		var hostScope = Establish( hostProcessStore, hostConnection );
		hostProcessStore.ApplyState( hostScope, State(
			hostConnection, 1, 1, hostCharacter, new[] { Inventory( "Host inventory" ) } ) );

		var clientNonce = ClientSessionNonce.New();
		clientScopeStore.PrepareSession( clientNonce );

		Assert.AreEqual( ClientLifecycleState.CharacterActive, hostProcessStore.Lifecycle );
		Assert.HasCount( 1, hostProcessStore.Inventories );
		Assert.AreEqual( ClientLifecycleState.Disconnected, clientScopeStore.Lifecycle );
		Assert.IsNull( clientScopeStore.State );
		Assert.AreEqual( 1L, clientScopeStore.Version );
		Assert.IsEmpty( typeof(HexClientStore).GetFields(
			System.Reflection.BindingFlags.Static |
			System.Reflection.BindingFlags.Public |
			System.Reflection.BindingFlags.NonPublic) );
	}

	[TestMethod]
	public void OutOfOrderUniqueChatDeliveriesMergeWithoutLoweringRevisionOrDuplicatingMessages()
	{
		var store = new HexClientStore();
		var connection = ConnectionEpoch.New();
		var scope = Establish( store, connection );
		var epoch = new ChatDeliveryEpoch( connection, 1 );
		Assert.IsTrue( store.ApplyState( scope, State( connection, 1, 1, CharacterId.New() ) ) );
		var first = Message( "first", DateTimeOffset.UnixEpoch );
		var second = Message( "second", DateTimeOffset.UnixEpoch );
		var third = Message( "third", DateTimeOffset.UnixEpoch );
		var version = store.Version;

		store.ReplaceChat( scope, new ChatSnapshot( epoch, 4, new[] { first } ) );
		store.ReplaceChat( scope, new ChatSnapshot( epoch, 6, new[] { third } ) );
		store.ReplaceChat( scope, new ChatSnapshot( epoch, 5, new[] { second } ) );
		store.ReplaceChat( scope, new ChatSnapshot( epoch, 3, new[] { first } ) );

		Assert.AreEqual( 6L, store.Chat!.Revision );
		CollectionAssert.AreEqual( new[] { "first", "second", "third" },
			store.Chat.Messages.Select( message => message.Text ).ToArray() );
		Assert.AreEqual( version + 3, store.Version,
			"A delayed unique delivery must append; a delayed duplicate must not publish." );
	}

	[TestMethod]
	public void SameRevisionChunksMergeAndRetentionKeepsNewestBoundedHistory()
	{
		var store = new HexClientStore();
		var connection = ConnectionEpoch.New();
		var scope = Establish( store, connection );
		var epoch = new ChatDeliveryEpoch( connection, 1 );
		Assert.IsTrue( store.ApplyState( scope, State( connection, 1, 1, CharacterId.New() ) ) );
		var messages = Enumerable.Range( 0, HexClientStore.MaximumRetainedChatMessages + 25 )
			.Select( index => Message( index.ToString(), DateTimeOffset.UnixEpoch.AddSeconds( index ) ) )
			.ToArray();

		store.ReplaceChat( scope, new ChatSnapshot( epoch, 10, messages.Take( 150 ) ) );
		store.ReplaceChat( scope, new ChatSnapshot( epoch, 10, messages.Skip( 150 ) ) );

		Assert.AreEqual( 10L, store.Chat!.Revision );
		Assert.HasCount( HexClientStore.MaximumRetainedChatMessages, store.Chat.Messages );
		Assert.AreEqual( "25", store.Chat.Messages[0].Text );
		Assert.AreEqual( "224", store.Chat.Messages[^1].Text );
	}

	[TestMethod]
	public void ReceiverStoresRemainIsolatedWhenHostPublishesDifferentRecipientSnapshots()
	{
		var firstReceiver = new HexClientStore();
		var secondReceiver = new HexClientStore();
		var firstConnection = ConnectionEpoch.New();
		var secondConnection = ConnectionEpoch.New();
		var firstScope = Establish( firstReceiver, firstConnection );
		var secondScope = Establish( secondReceiver, secondConnection );
		var firstEpoch = new ChatDeliveryEpoch( firstConnection, 1 );
		var secondEpoch = new ChatDeliveryEpoch( secondConnection, 1 );
		Assert.IsTrue( firstReceiver.ApplyState( firstScope, State( firstConnection, 1, 1, CharacterId.New() ) ) );
		Assert.IsTrue( secondReceiver.ApplyState( secondScope, State( secondConnection, 1, 1, CharacterId.New() ) ) );
		firstReceiver.ReplaceChat( firstScope, new ChatSnapshot( firstEpoch, 1, new[] { Message( "first-only", DateTimeOffset.UnixEpoch ) } ) );
		secondReceiver.ReplaceChat( secondScope, new ChatSnapshot( secondEpoch, 1, new[] { Message( "second-only", DateTimeOffset.UnixEpoch ) } ) );
		firstReceiver.ReplaceChat( firstScope, new ChatSnapshot( firstEpoch, 2, new[] { Message( "first-next", DateTimeOffset.UnixEpoch.AddSeconds( 1 ) ) } ) );

		CollectionAssert.AreEqual( new[] { "first-only", "first-next" },
			firstReceiver.Chat!.Messages.Select( message => message.Text ).ToArray() );
		CollectionAssert.AreEqual( new[] { "second-only" },
			secondReceiver.Chat!.Messages.Select( message => message.Text ).ToArray() );
	}

	[TestMethod]
	public void PriorCharacterAndConnectionEpochDeliveriesCannotEnterTheCurrentView()
	{
		var store = new HexClientStore();
		var connection = ConnectionEpoch.New();
		var scope = Establish( store, connection );
		var firstCharacter = CharacterId.New();
		var secondCharacter = CharacterId.New();
		Assert.IsTrue( store.ApplyState( scope, State( connection, 1, 1, firstCharacter ) ) );
		store.ReplaceChat( scope, new ChatSnapshot(
			new ChatDeliveryEpoch( connection, 1 ), 8,
			new[] { Message( "first-character", DateTimeOffset.UnixEpoch ) } ) );
		Assert.IsTrue( store.ApplyState( scope, State( connection, 2, 2, secondCharacter ) ) );
		var version = store.Version;

		store.ReplaceChat( scope, new ChatSnapshot(
			new ChatDeliveryEpoch( connection, 1 ), 100,
			new[] { Message( "delayed-character", DateTimeOffset.UnixEpoch ) } ) );
		store.ReplaceChat( scope, new ChatSnapshot(
			new ChatDeliveryEpoch( ConnectionEpoch.New(), 2 ), 101,
			new[] { Message( "delayed-connection", DateTimeOffset.UnixEpoch ) } ) );

		Assert.IsNull( store.Chat );
		Assert.AreEqual( version, store.Version );

		var current = new ChatSnapshot(
			new ChatDeliveryEpoch( connection, 2 ), 1,
			new[] { Message( "current", DateTimeOffset.UnixEpoch ) } );
		store.ReplaceChat( scope, current );
		Assert.AreSame( current, store.Chat );
		store.ClearSession();
		var clearedVersion = store.Version;
		store.ReplaceChat( scope, current );
		Assert.IsNull( store.Chat );
		Assert.AreEqual( clearedVersion, store.Version );
	}

	[TestMethod]
	public void ClearAndReconnectRejectEveryPacketFromThePriorNonceAndHello()
	{
		var store = new HexClientStore();
		var oldConnection = ConnectionEpoch.New();
		var oldScope = Establish( store, oldConnection );
		Assert.IsTrue( store.ApplyState( oldScope, State( oldConnection, 1, 1, CharacterId.New() ) ) );
		store.ClearSession();

		var newNonce = ClientSessionNonce.New();
		var newScope = new ClientSessionScope( newNonce, ConnectionEpoch.New() );
		store.PrepareSession( newNonce );
		Assert.IsFalse( store.AcceptHello( new ClientSessionHello( oldScope ) ) );
		Assert.IsTrue( store.AcceptHello( new ClientSessionHello( newScope ) ) );
		Assert.IsFalse( store.Accepts( oldScope ), "Delayed command results are gated by the same exact scope." );

		Assert.IsFalse( store.ApplyState( oldScope, State( oldConnection, 2, 2, CharacterId.New() ) ) );
		Assert.IsFalse( store.ReplaceCharacterList( oldScope, new CharacterListSnapshot( 99, Array.Empty<CharacterSummarySnapshot>() ) ) );
		Assert.IsFalse( store.ReplaceChat( oldScope, new ChatSnapshot(
			new ChatDeliveryEpoch( oldConnection, 1 ), 99, Array.Empty<ChatMessageSnapshot>() ) ) );
		Assert.IsNull( store.State );
		Assert.IsNull( store.CharacterList );
		Assert.IsNull( store.Chat );
	}

	[TestMethod]
	public void ExactDuplicateHelloIsAnIdempotentNoOpButOtherScopesAreRejected()
	{
		var store = new HexClientStore();
		var scope = Establish( store, ConnectionEpoch.New() );
		var version = store.Version;

		Assert.IsTrue( store.AcceptHello( new ClientSessionHello( scope ) ) );
		Assert.AreEqual( version, store.Version );
		Assert.AreEqual( scope, store.Scope );

		Assert.IsFalse( store.AcceptHello( new ClientSessionHello(
			new ClientSessionScope( scope.Nonce, ConnectionEpoch.New() ) ) ) );
		Assert.IsFalse( store.AcceptHello( new ClientSessionHello(
			new ClientSessionScope( ClientSessionNonce.New(), scope.Connection ) ) ) );
		Assert.AreEqual( version + 2, store.Version );
		Assert.AreEqual( scope, store.Scope );
	}

	[TestMethod]
	public void SessionRevocationIsExactScopedAndClearsAllPublishedState()
	{
		var store = new HexClientStore();
		var connection = ConnectionEpoch.New();
		var scope = Establish( store, connection );
		var character = CharacterId.New();
		Assert.IsTrue( store.ApplyState( scope, State( connection, 1, 1, character ) ) );
		Assert.IsTrue( store.ReplaceCharacterList(
			scope,
			new CharacterListSnapshot( 1, Array.Empty<CharacterSummarySnapshot>() ) ) );

		var stale = new ClientSessionScope( ClientSessionNonce.New(), scope.Connection );
		Assert.IsFalse( store.RevokeSession( stale ) );
		Assert.AreEqual( scope, store.Scope );
		Assert.IsNotNull( store.State );

		Assert.IsTrue( store.RevokeSession( scope ) );
		Assert.AreEqual( ClientLifecycleState.Disconnected, store.Lifecycle );
		Assert.IsNull( store.Scope );
		Assert.IsNull( store.State );
		Assert.IsNull( store.CharacterList );
		Assert.IsNull( store.Chat );
		Assert.IsFalse( store.RevokeSession( scope ) );
	}

	private static ClientSessionScope Establish( HexClientStore store, ConnectionEpoch connection )
	{
		var nonce = ClientSessionNonce.New();
		var scope = new ClientSessionScope( nonce, connection );
		store.PrepareSession( nonce );
		Assert.IsTrue( store.AcceptHello( new ClientSessionHello( scope ) ) );
		return scope;
	}

	private static ClientStateSnapshot State(
		ConnectionEpoch connection,
		long characterEpoch,
		long revision,
		CharacterId? characterId,
		IEnumerable<InventorySnapshot>? inventories = null,
		ActionProgressSnapshot? action = null,
		PlayerRosterSnapshot? roster = null,
		IEnumerable<SchemaViewSnapshot>? schemaViews = null ) => new(
		new ClientStateEpoch( connection, characterEpoch, revision ),
		Public( characterId ),
		characterId is null ? null : Private( characterId.Value ),
		roster ?? PlayerRosterSnapshot.Empty,
		schemaViews,
		inventories,
		action );

	private static PlayerPublicSnapshot Public( CharacterId? characterId ) => new(
		ConnectionId.New(),
		7656119,
		"Player",
		characterId,
		characterId is null ? string.Empty : "Alyx Vance",
		characterId is null ? string.Empty : "Description",
		characterId is null ? null : new DefinitionId( "citizen_model" ),
		characterId is null ? null : new FactionId( "citizen" ),
		null,
		false,
		false );

	[TestMethod]
	public void OwnerFacingCharacterNameDoesNotBecomeTheDefaultReplicatedLabel()
	{
		var snapshot = Public( CharacterId.New() );

		Assert.AreEqual( "Alyx Vance", snapshot.CharacterName );
		Assert.AreEqual( "Unknown citizen", snapshot.ReplicatedCharacterName );
	}

	private static PlayerPrivateSnapshot Private( CharacterId characterId ) => new(
		characterId,
		100,
		InventoryId.New(),
		new Dictionary<string, SnapshotValue>( StringComparer.Ordinal )
		{
			["cid"] = SnapshotValue.String( "12345" )
		} );

	private static CharacterListSnapshot CharacterList( CharacterId characterId ) => new(
		1,
		new[]
		{
			new CharacterSummarySnapshot(
				characterId,
				0,
				"Alyx Vance",
				"Description",
				new DefinitionId( "citizen_model" ),
				new FactionId( "citizen" ),
				null,
				DateTimeOffset.UnixEpoch,
				false )
		} );

	private static InventorySnapshot Inventory( string title = "Inventory", InventoryId? id = null ) => new(
		id ?? InventoryId.New(),
		1,
		InventoryViewKind.Main,
		title,
		4,
		4,
		Array.Empty<InventoryItemSnapshot>() );

	private static ChatSnapshot Chat( ChatDeliveryEpoch epoch ) => new(
		epoch,
		1,
		new[]
		{
			new ChatMessageSnapshot(
				Guid.NewGuid(), "ic", CharacterId.New(), "Speaker", "Hello",
				DateTimeOffset.UnixEpoch )
		} );

	private static ChatMessageSnapshot Message( string text, DateTimeOffset sentAt ) => new(
		Guid.NewGuid(), "ic", CharacterId.New(), "Speaker", text, sentAt );

	private static ActionProgressSnapshot Action() => new(
		Guid.NewGuid(),
		new ActionId( "use" ),
		"Using",
		DateTimeOffset.UnixEpoch,
		TimeSpan.FromSeconds( 3 ),
		true );
}
#nullable enable

using System;
using Hexagon.V2.Composition;
using Hexagon.V2.Kernel;

namespace Hexagon.V2.Tests.Composition;

[TestClass]
public sealed class HostBootstrapResolutionTests
{
	[TestMethod]
	public void ProbeWithoutAnIsolatedPersistenceRootFailsTheBootstrapClosed()
	{
		var fromOverride = HostBootstrapResolution.Resolve(
			overrideRoot: "", sceneRoot: "", overrideProbe: "commerce", sceneProbe: "", Normalize );
		var fromScene = HostBootstrapResolution.Resolve(
			overrideRoot: "", sceneRoot: "", overrideProbe: "", sceneProbe: "commerce", Normalize );

		Assert.AreEqual( ErrorCode.ConfigurationInvalid, fromOverride.Error!.Code );
		Assert.AreEqual( ErrorCode.ConfigurationInvalid, fromScene.Error!.Code );
	}

	[TestMethod]
	public void ProbePairedWithAnIsolatedRootResolves()
	{
		var resolved = HostBootstrapResolution.Resolve(
			overrideRoot: "verify/run-1", sceneRoot: "", overrideProbe: " commerce ", sceneProbe: "", Normalize );

		Assert.IsTrue( resolved.Succeeded, resolved.Error?.Message );
		Assert.AreEqual( "normalized:verify/run-1", resolved.Value.PersistenceRootOverride );
		Assert.AreEqual( "commerce", resolved.Value.VerificationProbe, "The probe value is trimmed." );
	}

	[TestMethod]
	public void BlankProbeAndBlankRootResolveToProductionDefaults()
	{
		var resolved = HostBootstrapResolution.Resolve( "", "", "", "", Normalize );

		Assert.IsTrue( resolved.Succeeded );
		Assert.AreEqual( string.Empty, resolved.Value.PersistenceRootOverride );
		Assert.AreEqual( string.Empty, resolved.Value.VerificationProbe );
	}

	[TestMethod]
	public void LaunchOverridesTakePrecedenceOverSceneAuthoredValues()
	{
		var resolved = HostBootstrapResolution.Resolve(
			overrideRoot: "override-root",
			sceneRoot: "scene-root",
			overrideProbe: "override-probe",
			sceneProbe: "scene-probe",
			Normalize );

		Assert.IsTrue( resolved.Succeeded );
		Assert.AreEqual( "normalized:override-root", resolved.Value.PersistenceRootOverride );
		Assert.AreEqual( "override-probe", resolved.Value.VerificationProbe );
	}

	[TestMethod]
	public void OverlongProbeIsTruncatedToTheContractLength()
	{
		var resolved = HostBootstrapResolution.Resolve(
			"root", "", new string( 'p', HostBootstrapResolution.MaximumProbeLength + 40 ), "", Normalize );

		Assert.IsTrue( resolved.Succeeded );
		Assert.AreEqual( HostBootstrapResolution.MaximumProbeLength, resolved.Value.VerificationProbe.Length );
	}

	[TestMethod]
	public void ThrowingRootNormalizationFailsClosedAsConfigurationInvalid()
	{
		var resolved = HostBootstrapResolution.Resolve(
			"..\\escape", "", "", "",
			static _ => throw new ArgumentException( "Persistence root escapes the data directory." ) );

		Assert.AreEqual( ErrorCode.ConfigurationInvalid, resolved.Error!.Code );
		StringAssert.Contains( resolved.Error.Message, "escapes" );
	}

	private static string Normalize( string root ) => $"normalized:{root}";
}
using Hexagon.V2.Kernel;
using Hexagon.V2.Kernel.Configuration;
using Hexagon.V2.Kernel.Definitions;
using Hexagon.V2.Kernel.Persistence;
using Hexagon.V2.Kernel.Schema;

namespace Hexagon.V2.Tests.Kernel;

[TestClass]
public sealed class SchemaCompilerTests
{
	[TestMethod]
	public void CompileOrdersModulesDeterministicallyAndResolvesDefinitions()
	{
		var schema = new DelegateSchema("test_schema", builder =>
		{
			builder.AddModule(new DelegateModule("zeta", _ => { }));
			builder.AddModule(new DelegateModule("gamma", module => module.DependsOn("beta")));
			builder.AddModule(new DelegateModule("beta", module =>
			{
				module.DependsOn("alpha");
				module.RegisterPermission(new PermissionDefinition("admin"));
				module.RegisterCommand(new CommandDefinition("ban", "admin"));
			}));
			builder.AddModule(new DelegateModule("alpha", module =>
			{
				module.RegisterFaction(new FactionDefinition("citizen", true, "worker"));
				module.RegisterClass(new ClassDefinition("worker", "citizen"));
			}));
		});

		var result = SchemaCompiler.Compile(schema);

		Assert.IsTrue(result.Succeeded, result.Error?.Message);
		CollectionAssert.AreEqual(
			new[] { "alpha", "beta", "gamma", "zeta" },
			result.Value.Modules.Select(x => x.Id).ToArray());
		Assert.AreEqual("ban", result.Value.Commands.Require("ban").Value.Id);
		Assert.AreEqual( CommandCostClass.Standard, result.Value.Commands.Require( "ban" ).Value.Cost );
	}

	[TestMethod]
	public void ValidationReportsCyclesMissingReferencesAndDuplicateDefinitions()
	{
		var schema = new DelegateSchema("test_schema", builder =>
		{
			builder.RegisterAction(new ActionDefinition("use"));
			builder.RegisterAction(new ActionDefinition("use"));
			builder.RegisterClass(new ClassDefinition("unit", "missing_faction"));
			builder.RegisterItem(ItemDefinition.Create("broken_item", true, null, "missing_action"));
			builder.AddModule(new DelegateModule("first", module => module.DependsOn("second")));
			builder.AddModule(new DelegateModule("second", module => module.DependsOn("first")));
			builder.AddModule(new DelegateModule("third", module => module.DependsOn("absent")));
		});

		var report = SchemaCompiler.Validate(schema);

		Assert.IsFalse(report.IsValid);
		Assert.IsTrue(report.Issues.Any(x => x.Code == ErrorCode.DependencyCycle));
		Assert.IsTrue(report.Issues.Any(x => x.Code == ErrorCode.MissingDependency));
		Assert.IsTrue(report.Issues.Any(x => x.Code == ErrorCode.DuplicateRegistration));
		Assert.IsTrue(report.Issues.Any(x => x.Code == ErrorCode.UnknownDefinition));
		Assert.IsTrue(report.Issues.Any(x => x.Message.Contains("world model", StringComparison.Ordinal)));
	}

	[TestMethod]
	public void UnknownDefinitionAndConfigTypeMismatchFailClosed()
	{
		var schema = new DelegateSchema("test_schema", builder =>
		{
			builder.RegisterAction(new ActionDefinition("use"));
			builder.RegisterConfig(new ConfigDefinition<int>("max_characters", 5, ConfigCodecs.Int32));
		});
		var compiled = SchemaCompiler.Compile(schema).Value;

		var missing = compiled.Actions.Require("equip");
		var wrongType = compiled.Configs.Require<long>("max_characters");

		Assert.IsTrue(missing.Failed);
		Assert.AreEqual(ErrorCode.UnknownDefinition, missing.Error!.Code);
		Assert.IsTrue(wrongType.Failed);
		Assert.AreEqual(ErrorCode.ConfigurationTypeMismatch, wrongType.Error!.Code);
	}

	[TestMethod]
	public void PersistedRegistrationsRequireUniqueConcreteTypesAndPositiveVersions()
	{
		var schema = new DelegateSchema("test_schema", builder =>
		{
			builder.RegisterPersistedType(new PersistedTypeRegistration("first", typeof(Payload), 1));
			builder.RegisterPersistedType(new PersistedTypeRegistration("second", typeof(Payload), 1));
			builder.RegisterPersistedType(new PersistedTypeRegistration("abstract", typeof(AbstractPayload), 0));
		});

		var report = SchemaCompiler.Validate(schema);

		Assert.IsTrue(report.Issues.Any(x => x.Code == ErrorCode.DuplicateRegistration));
		Assert.IsTrue(report.Issues.Any(x => x.Code == ErrorCode.PersistedTypeInvalid &&
			x.Message.Contains("positive version", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(x => x.Code == ErrorCode.PersistedTypeInvalid &&
			x.Message.Contains("closed concrete", StringComparison.Ordinal)));
	}

	[TestMethod]
	public void InvalidConfigDefaultFailsSchemaConformance()
	{
		var schema = new DelegateSchema("test_schema", builder =>
			builder.RegisterConfig(new ConfigDefinition<int>("slots", 0, ConfigCodecs.Int32,
				value => value > 0
					? OperationResult.Success()
					: OperationResult.Failure(ErrorCode.ConfigurationInvalid, "Slots must be positive."))));

		var report = SchemaCompiler.Validate(schema);

		Assert.IsTrue(report.Issues.Any(x => x.Code == ErrorCode.ConfigurationInvalid));
	}

	[TestMethod]
	public void CharacterFieldKindsRejectMismatchedDefaultsWithoutReflection()
	{
		var schema = new DelegateSchema("test_schema", builder =>
			builder.RegisterCharacterField(new CharacterFieldDefinition(
				"age", CharacterFieldValueKind.Integer, true, true, "not-an-integer")));

		var report = SchemaCompiler.Validate(schema);

		Assert.IsTrue(report.Issues.Any(x => x.Path == "character_fields.age" &&
			x.Message.Contains("does not match Integer", StringComparison.Ordinal)));
	}

	[TestMethod]
	public void CommandAdmissionCostMustBeADeclaredWeight()
	{
		var schema = new DelegateSchema( "test_schema", builder =>
			builder.RegisterCommand( new CommandDefinition(
				"invalid", null, (CommandCostClass)16 ) ) );

		var report = SchemaCompiler.Validate( schema );

		Assert.IsTrue( report.Issues.Any( issue =>
			issue.Code == ErrorCode.SchemaInvalid && issue.Path == "commands.invalid.cost" ) );
	}

	private sealed record Payload(string Value);
	private abstract record AbstractPayload;

	private sealed class DelegateSchema : IHexSchema
	{
		private readonly Action<SchemaBuilder> _configure;

		public DelegateSchema(string id, Action<SchemaBuilder> configure)
		{
			Id = id;
			_configure = configure;
		}

		public string Id { get; }
		public void Configure(SchemaBuilder builder) => _configure(builder);
	}

	private sealed class DelegateModule : IHexModule
	{
		private readonly Action<ModuleBuilder> _configure;

		public DelegateModule(string id, Action<ModuleBuilder> configure)
		{
			Id = id;
			_configure = configure;
		}

		public string Id { get; }
		public void Configure(ModuleBuilder builder) => _configure(builder);
	}
}
#nullable enable

using Hexagon.V2.Networking;
using Hexagon.V2.Kernel;

namespace Hexagon.V2.Tests.Networking;

[TestClass]
public sealed class CommandAdmissionTests
{
	[TestMethod]
	public void WeightedBucketRefillsExactlyAndRejectedAttemptsAreNotRefunded()
	{
		var admission = new CommandAdmissionController( 1_000 );
		var first = admission.TryBegin( CommandRequestId.New(), 16, 0 );
		var rejected = admission.TryBegin( CommandRequestId.New(), 1, 0 );
		var halfRefill = admission.TryBegin( CommandRequestId.New(), 4, 500 );
		var noRefund = admission.TryBegin( CommandRequestId.New(), 1, 500 );

		Assert.IsTrue( first.Accepted );
		Assert.AreEqual( CommandAdmissionFailure.RateLimited, rejected.Failure );
		Assert.AreEqual( TimeSpan.FromMilliseconds( 125 ), rejected.RetryAfter );
		Assert.IsTrue( halfRefill.Accepted );
		Assert.AreEqual( CommandAdmissionFailure.RateLimited, noRefund.Failure );
	}

	[TestMethod]
	public void ActiveCapIsSixteenAndTheSeventeenthAttemptStillConsumesItsToken()
	{
		var admission = new CommandAdmissionController( 1_000 );
		for ( var index = 0; index < CommandAdmissionController.MaximumActiveRequests; index++ )
			Assert.IsTrue( admission.TryBegin( CommandRequestId.New(), 1, index * 125 ).Accepted );

		var before = admission.AvailableUnits;
		var overflow = admission.TryBegin( CommandRequestId.New(), 1, 2_000 );

		Assert.AreEqual( CommandAdmissionFailure.RateLimited, overflow.Failure );
		Assert.AreEqual( CommandAdmissionController.MaximumActiveRequests, admission.ActiveCount );
		Assert.AreEqual( before, admission.AvailableUnits, 0.0001,
			"The one-unit refill was consumed before the active-cap rejection." );
	}

	[TestMethod]
	public void DuplicateRequestIsChargedBeforeReplayRejection()
	{
		var admission = new CommandAdmissionController( 1_000 );
		var request = CommandRequestId.New();
		Assert.IsTrue( admission.TryBegin( request, 2, 0 ).Accepted );
		Assert.IsTrue( admission.Finish( request ) );
		var before = admission.AvailableUnits;

		var duplicate = admission.TryBegin( request, 4, 0 );

		Assert.AreEqual( CommandAdmissionFailure.Duplicate, duplicate.Failure );
		Assert.AreEqual( before - 4, admission.AvailableUnits, 0.0001 );
	}

	[TestMethod]
	public void MalformedPayloadValidationOccursAfterAdmissionAndDoesNotRefund()
	{
		var admission = new CommandAdmissionController( 1_000 );
		var admitted = admission.TryBegin( CommandRequestId.New(), 2, 0 );
		var malformed = ClientPayloadLimits.Validate(
			new SendChatCommand( "ic", new string( 'x', ClientPayloadLimits.MaximumStringCharacters + 1 ) ) );

		Assert.IsTrue( admitted.Accepted );
		Assert.AreEqual( ErrorCode.InvalidArgument, malformed.Error!.Code );
		Assert.AreEqual( CommandAdmissionController.BurstUnits - 2, admission.AvailableUnits, 0.0001 );
	}

	[TestMethod]
	public void AuthenticatedIngressChargesBeforeSessionScopeValidationAndUnauthenticatedIngressDoesNotCharge()
	{
		var admission = new CommandAdmissionController( 1_000 );
		var order = new List<string>();
		var unauthenticated = CommandIngressAdmission.Evaluate<object>(
			false,
			() =>
			{
				order.Add( "admission" );
				return admission.TryBegin( CommandRequestId.New(), 4, 0 );
			},
			() =>
			{
				order.Add( "session" );
				return OperationResult<object>.Success( new object() );
			},
			() => order.Add( "finish" ) );

		Assert.IsFalse( unauthenticated.Authenticated );
		Assert.IsFalse( unauthenticated.SessionEvaluated );
		Assert.IsEmpty( order );
		Assert.AreEqual( CommandAdmissionController.BurstUnits, admission.AvailableUnits, 0.0001 );

		var requestId = CommandRequestId.New();
		var forgedScope = CommandIngressAdmission.Evaluate<object>(
			true,
			() =>
			{
				order.Add( "admission" );
				return admission.TryBegin( requestId, 4, 0 );
			},
			() =>
			{
				order.Add( "session" );
				return OperationResult<object>.Failure( ErrorCode.Unauthorized, "stale scope" );
			},
			() =>
			{
				order.Add( "finish" );
				Assert.IsTrue( admission.Finish( requestId ) );
			} );

		Assert.IsTrue( forgedScope.Authenticated );
		Assert.IsTrue( forgedScope.Admission.Accepted );
		Assert.IsTrue( forgedScope.SessionEvaluated );
		Assert.AreEqual( ErrorCode.Unauthorized, forgedScope.Session!.Value.Error!.Code );
		CollectionAssert.AreEqual( new[] { "admission", "session", "finish" }, order );
		Assert.AreEqual( CommandAdmissionController.BurstUnits - 4, admission.AvailableUnits, 0.0001 );
		Assert.AreEqual( 0, admission.ActiveCount );
	}

	[TestMethod]
	public void AuthenticatedIngressPreservesRateLimitRetryAndSkipsSessionValidationWhenRejected()
	{
		var admission = new CommandAdmissionController( 1_000 );
		Assert.IsTrue( admission.TryBegin( CommandRequestId.New(), CommandAdmissionController.BurstUnits, 0 ).Accepted );
		var sessionEvaluated = false;

		var rejected = CommandIngressAdmission.Evaluate<object>(
			true,
			() => admission.TryBegin( CommandRequestId.New(), 1, 0 ),
			() =>
			{
				sessionEvaluated = true;
				return OperationResult<object>.Success( new object() );
			},
			() => Assert.Fail( "A request rejected by admission was never active and cannot be finished." ) );

		Assert.IsTrue( rejected.Authenticated );
		Assert.AreEqual( CommandAdmissionFailure.RateLimited, rejected.Admission.Failure );
		Assert.AreEqual( TimeSpan.FromMilliseconds( 125 ), rejected.Admission.RetryAfter );
		Assert.IsFalse( rejected.SessionEvaluated );
		Assert.IsFalse( sessionEvaluated );
	}

	[TestMethod]
	public void SchemaCostResolutionBoundsLookupAndChargesMalformedAttemptsAsUnknown()
	{
		var lookupCount = 0;
		int? ResolveKnown( string id )
		{
			lookupCount++;
			return id switch { "cheap" => 1, "standard" => 2, "expensive" => 4, _ => null };
		}

		Assert.AreEqual( 1, SchemaCommandAdmissionCost.Resolve( "cheap", ResolveKnown ) );
		Assert.AreEqual( 2, SchemaCommandAdmissionCost.Resolve( "standard", ResolveKnown ) );
		Assert.AreEqual( 4, SchemaCommandAdmissionCost.Resolve( "expensive", ResolveKnown ) );
		Assert.AreEqual( 8, SchemaCommandAdmissionCost.Resolve( "unknown", ResolveKnown ) );
		Assert.AreEqual( 4, lookupCount );

		foreach ( var malformedId in new[]
		{
			new string( 'x', ClientPayloadLimits.MaximumIdentifierCharacters + 1 ),
			"\ud800"
		} )
		{
			var lookupsBefore = lookupCount;
			var cost = SchemaCommandAdmissionCost.Resolve( malformedId, ResolveKnown );
			var admission = new CommandAdmissionController( 1_000 );
			var admitted = admission.TryBegin( CommandRequestId.New(), cost, 0 );
			var validation = ClientPayloadLimits.Validate(
				new RunSchemaCommandCommand( malformedId, new Dictionary<string, SnapshotValue>() ) );

			Assert.AreEqual( SchemaCommandAdmissionCost.UnknownCommandUnits, cost );
			Assert.AreEqual( lookupsBefore, lookupCount,
				"Malformed identifiers must not reach the schema registry lookup." );
			Assert.IsTrue( admitted.Accepted );
			Assert.AreEqual( ErrorCode.InvalidArgument, validation.Error!.Code );
			Assert.AreEqual( CommandAdmissionController.BurstUnits - SchemaCommandAdmissionCost.UnknownCommandUnits,
				admission.AvailableUnits, 0.0001 );
		}
	}

	[TestMethod]
	public void RateLimitedOperationResultRoundTripsAnExplicitBoundedRetryDelay()
	{
		var hostResult = OperationResultWireContract.RateLimited(
			"budget exhausted", TimeSpan.FromMilliseconds( 125.25 ) );
		var wireDelay = OperationResultWireContract.EncodeRetryAfterMilliseconds( hostResult );
		var clientResult = OperationResultWireContract.Decode(
			false, (int)ErrorCode.RateLimited, hostResult.Error!.Message, wireDelay );

		Assert.AreEqual( 126, wireDelay );
		Assert.AreEqual( ErrorCode.RateLimited, clientResult.Error!.Code );
		Assert.AreEqual(
			"126",
			clientResult.Error.Details![OperationResultWireContract.RetryAfterMillisecondsDetail] );

		var clamped = OperationResultWireContract.Decode(
			false, (int)ErrorCode.RateLimited, "slow down", int.MaxValue );
		Assert.AreEqual(
			OperationResultWireContract.MaximumRetryAfterMilliseconds.ToString( System.Globalization.CultureInfo.InvariantCulture ),
			clamped.Error!.Details![OperationResultWireContract.RetryAfterMillisecondsDetail] );
	}

	[TestMethod]
	public void OperationResultWireDefaultsMalformedRetryDelayAndIgnoresItForOtherFailures()
	{
		var malformedRateLimit = OperationResult.Failure(
			ErrorCode.RateLimited,
			"budget exhausted",
			new Dictionary<string, string>( StringComparer.Ordinal )
			{
				[OperationResultWireContract.RetryAfterMillisecondsDetail] = "not-a-duration"
			} );
		Assert.AreEqual(
			OperationResultWireContract.DefaultRetryAfterMilliseconds,
			OperationResultWireContract.EncodeRetryAfterMilliseconds( malformedRateLimit ) );

		var conflict = OperationResultWireContract.Decode(
			false, (int)ErrorCode.Conflict, "conflict", 999 );
		Assert.IsNull( conflict.Error!.Details );
		Assert.AreEqual( 0, OperationResultWireContract.EncodeRetryAfterMilliseconds( conflict ) );
	}

	[TestMethod]
	public void ReconciliationPendingErrorCodeRoundTripsWithoutDowngrade()
	{
		var decoded = OperationResultWireContract.Decode(
			false,
			(int)ErrorCode.ReconciliationPending,
			"committed and pending",
			0 );

		Assert.AreEqual( ErrorCode.ReconciliationPending, decoded.Error!.Code );
	}

	[TestMethod]
	public async Task ParallelBeginFinishAndDisconnectPreserveTheActiveCapAndState()
	{
		for ( var iteration = 0; iteration < 50; iteration++ )
		{
			var admission = new CommandAdmissionController( 1_000 );
			var requests = Enumerable.Range( 0, 64 ).Select( _ => CommandRequestId.New() ).ToArray();
			var results = await Task.WhenAll( requests.Select( request =>
				Task.Run( () => admission.TryBegin( request, 1, 0 ) ) ) );
			Assert.AreEqual( CommandAdmissionController.MaximumActiveRequests,
				results.Count( result => result.Accepted ) );
			Assert.AreEqual( CommandAdmissionController.MaximumActiveRequests, admission.ActiveCount );

			await Task.WhenAll( requests.Select( request => Task.Run( () => admission.Finish( request ) ) ) );
			Assert.AreEqual( 0, admission.ActiveCount );

			await Task.WhenAll(
				Task.Run( admission.Disconnect ),
				Task.Run( () => admission.TryBegin( CommandRequestId.New(), 1, 1_000 ) ) );
			Assert.IsLessThanOrEqualTo( CommandAdmissionController.MaximumActiveRequests, admission.ActiveCount );
		}
	}
}
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Hexagon.V2.Persistence;

namespace Hexagon.V2.Tests.Persistence;

internal sealed record TestDocument( string Name, int Score );

internal sealed record CollectionTestDocument
{
	public string Name { get; init; } = "";
	public IReadOnlyList<string> Tags { get; init; } = Array.Empty<string>();
}

internal sealed class MutablePersistedTestDocument
{
	public string Name { get; set; } = "";
	public List<string> Tags { get; init; } = new();
}

[PersistedType( "test.marked", 1 )]
internal sealed record MarkedButUnregisteredTestDocument( string Name );

internal abstract record AbstractTestDocument( string Name );

internal static class PersistenceTestSupport
{
	public static PersistedTypeRegistry CreateRegistry() => new PersistedTypeRegistry()
		.Register<TestDocument>( new PersistedTypeKey( "test.document" ), 1,
			PersistedValuePublication.Immutable )
		.Register<CollectionTestDocument>( new PersistedTypeKey( "test.collection" ), 1,
			static value => value with
			{
				Tags = PersistedValuePublication.ReadOnlyList( value.Tags )
			} );

	public static FileSystemPersistenceProvider CreateFileProvider(
		IPersistenceStorage storage,
		int checkpointEveryCommits = 0,
		bool quarantineCorruptStore = false ) => new(
		storage,
		new FileSystemPersistenceOptions( "test-schema" )
		{
			CheckpointEveryCommits = checkpointEveryCommits,
			RetainedCheckpointGenerations = 2,
			QuarantineCorruptStore = quarantineCorruptStore
		},
		CreateRegistry() );
}

internal sealed class FaultInjectingStorage : IPersistenceStorage
{
	private readonly InMemoryPersistenceStorage _inner = new();
	private TaskCompletionSource? _immutableStarted;
	private TaskCompletionSource? _immutableRelease;

	public Func<string, bool>? FailNextImmutableWrite { get; set; }
	public Func<string, bool>? FailNextRead { get; set; }
	public Func<string, bool>? FailNextDelete { get; set; }
	public bool FailNextLeaseDispose { get; set; }

	public async ValueTask<IPersistenceLease> AcquireExclusiveLeaseAsync(
		string path,
		CancellationToken cancellationToken = default )
	{
		var lease = await _inner.AcquireExclusiveLeaseAsync( path, cancellationToken );
		return new FaultInjectingLease( this, lease );
	}

	public Task BlockNextImmutableWrite()
	{
		_immutableStarted = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously );
		_immutableRelease = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously );
		return _immutableStarted.Task;
	}

	public void ReleaseBlockedImmutableWrite() =>
		(_immutableRelease ?? throw new InvalidOperationException( "No immutable write is blocked." )).TrySetResult();

	public async Task CorruptByteFromEndAsync( string path, int offsetFromEnd )
	{
		var content = await ReadAsync( path ) ?? throw new InvalidOperationException( $"'{path}' does not exist." );
		var bytes = content.ToArray();
		if ( offsetFromEnd <= 0 || offsetFromEnd > bytes.Length )
		{
			throw new ArgumentOutOfRangeException( nameof(offsetFromEnd) );
		}

		bytes[^offsetFromEnd] ^= 0x5A;
		await OverwriteAsync( path, bytes );
	}

	public async Task CorruptByteAsync( string path, int offset )
	{
		var content = await ReadAsync( path ) ?? throw new InvalidOperationException( $"'{path}' does not exist." );
		var bytes = content.ToArray();
		if ( offset < 0 || offset >= bytes.Length )
		{
			throw new ArgumentOutOfRangeException( nameof(offset) );
		}

		bytes[offset] ^= 0x5A;
		await OverwriteAsync( path, bytes );
	}

	public async Task OverwriteAsync( string path, ReadOnlyMemory<byte> content )
	{
		await _inner.DeleteAsync( path );
		if ( !await _inner.TryWriteImmutableAsync( path, content ) )
			throw new InvalidOperationException( $"Could not overwrite test path '{path}'." );
	}

	public async Task SeedAsync( string path, ReadOnlyMemory<byte> content )
	{
		if ( !await _inner.TryWriteImmutableAsync( path, content ) )
			throw new InvalidOperationException( $"Could not seed test path '{path}'." );
	}

	public ValueTask<bool> ExistsAsync( string path, CancellationToken cancellationToken = default ) =>
		_inner.ExistsAsync( path, cancellationToken );

	public ValueTask<ReadOnlyMemory<byte>?> ReadAsync( string path, CancellationToken cancellationToken = default )
	{
		if ( FailNextRead?.Invoke( path ) == true )
		{
			FailNextRead = null;
			throw new InvalidOperationException( "Injected read failure." );
		}
		return _inner.ReadAsync( path, cancellationToken );
	}

	public ValueTask<IReadOnlyList<string>> ListAsync( string prefix, CancellationToken cancellationToken = default ) =>
		_inner.ListAsync( prefix, cancellationToken );

	public async ValueTask<bool> TryWriteImmutableAsync(
		string path,
		ReadOnlyMemory<byte> content,
		CancellationToken cancellationToken = default )
	{
		if ( FailNextImmutableWrite?.Invoke( path ) == true )
		{
			FailNextImmutableWrite = null;
			throw new InvalidOperationException( "Injected immutable-write failure." );
		}
		if ( _immutableStarted is not null && _immutableRelease is not null )
		{
			var started = _immutableStarted;
			var release = _immutableRelease;
			_immutableStarted = null;
			started.TrySetResult();
			await release.Task.WaitAsync( cancellationToken );
			if ( ReferenceEquals( _immutableRelease, release ) ) _immutableRelease = null;
		}

		return await _inner.TryWriteImmutableAsync( path, content, cancellationToken );
	}

	public ValueTask DeleteAsync( string path, CancellationToken cancellationToken = default )
	{
		if ( FailNextDelete?.Invoke( path ) == true )
		{
			FailNextDelete = null;
			throw new InvalidOperationException( "Injected delete failure." );
		}
		return _inner.DeleteAsync( path, cancellationToken );
	}

	private bool ConsumeLeaseDisposeFailure()
	{
		if ( !FailNextLeaseDispose ) return false;
		FailNextLeaseDispose = false;
		return true;
	}

	private sealed class FaultInjectingLease : IPersistenceLease
	{
		private readonly FaultInjectingStorage _owner;
		private readonly IPersistenceLease _inner;

		public FaultInjectingLease( FaultInjectingStorage owner, IPersistenceLease inner )
		{
			_owner = owner;
			_inner = inner;
		}

		public string Path => _inner.Path;
		public bool IsReleased => _inner.IsReleased;

		public async ValueTask DisposeAsync()
		{
			if ( _owner.ConsumeLeaseDisposeFailure() )
				throw new InvalidOperationException( "Injected lease-disposal failure." );
			await _inner.DisposeAsync();
		}
	}
}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
using Sandbox;

namespace APLib.UnitTests;

[TestClass]
public class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		
		using ( scene.Push() )
		{
			var go = new GameObject();
			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace APLib.UnitTests;

[TestClass]
public class TestInit
{
	private static Sandbox.TestAppSystem s_appSystem;
	
	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		s_appSystem = new Sandbox.TestAppSystem();
		s_appSystem.Init();
	}
	
	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		s_appSystem.Shutdown();
	}
}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
#nullable enable

using System.Text.Json;
using Hexagon.V2.Application;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Kernel.Definitions;
using Hexagon.V2.Kernel.Persistence;
using Hexagon.V2.Kernel.Schema;
using Hexagon.V2.Persistence;

namespace Hexagon.V2.Tests.Application;

[TestClass]
public sealed class DomainInvariantValidatorTests
{
	[TestMethod]
	public async Task ValidCharacterInventoryAndLocationGraphProducesCleanReport()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId(9001);
		var character = ApplicationServiceTestEnvironment.Character(account, 0);
		var item = ApplicationServiceTestEnvironment.Item();
		var inventory = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(character.Id),
			new[] { new InventoryPlacement(item.Id, 0, 0) });
		await environment.SeedAsync(unitOfWork =>
		{
			unitOfWork.Create(environment.Repositories.Characters, DomainKeys.Character(character.Id), character);
			unitOfWork.Create(
				environment.Repositories.CharacterLifecycleGuards,
				DomainKeys.CharacterLifecycleGuard(character.Id),
				new CharacterLifecycleGuardRecord { CharacterId = character.Id, ReferenceRevision = 0 });
			unitOfWork.Create(
				environment.Repositories.CharacterSlots,
				DomainKeys.CharacterSlot(account, 0),
				new CharacterSlotRecord { AccountId = account, Slot = 0, CharacterId = character.Id });
			unitOfWork.Create(environment.Repositories.Items, DomainKeys.Item(item.Id), item);
			unitOfWork.Create(environment.Repositories.Inventories, DomainKeys.Inventory(inventory.Id), inventory);
			unitOfWork.Create(
				environment.Repositories.OwnerInventories,
				DomainKeys.OwnerInventory(inventory.Owner, "main"),
				new OwnerInventoryRecord { Owner = inventory.Owner, Role = "main", InventoryId = inventory.Id });
		});
		var validator = new DomainInvariantValidator(
			environment.Repositories,
			environment.Schema,
			new SchemaItemShapeCatalog(environment.Schema, environment.Repositories),
			environment.PersistenceProfile);

		var report = validator.Validate();

		Assert.IsTrue(report.IsValid);
		Assert.IsEmpty(report.Issues);
	}

	[TestMethod]
	public async Task EmptyStoreStillRejectsIncompleteOrUnregisteredPersistenceProfile()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var profile = new SchemaPersistenceInvariantProfile(
			new PersistedTypeId("unknown.character-state"),
			new[] { ItemPersistenceContract.WithoutTraits(ApplicationServiceTestEnvironment.ItemDefinitionId) },
			Array.Empty<KeyValuePair<string, PersistedTypeId>>(),
			Array.Empty<KeyValuePair<string, PersistedTypeId>>());
		var report = new DomainInvariantValidator(
			environment.Repositories,
			environment.Schema,
			new SchemaItemShapeCatalog(environment.Schema, environment.Repositories),
			profile).Validate();

		Assert.IsFalse(report.IsValid);
		Assert.IsTrue(report.Issues.Any(issue =>
			issue.Path == "persistence-profile/character-state" &&
			issue.Code == ErrorCode.PersistedTypeInvalid));
		Assert.IsTrue(report.Issues.Any(issue =>
			issue.Path.EndsWith(ApplicationServiceTestEnvironment.BagDefinitionId, StringComparison.Ordinal) &&
			issue.Message.Contains("no persistence contract", StringComparison.Ordinal)));
	}

	[TestMethod]
	public async Task OutOfRangeCharacterSlotIsRejected()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId(9003);
		var character = ApplicationServiceTestEnvironment.Character(account, CharacterRules.MaximumSlots);
		await environment.SeedAsync(unitOfWork =>
		{
			unitOfWork.Create(environment.Repositories.Characters, DomainKeys.Character(character.Id), character);
			unitOfWork.Create(
				environment.Repositories.CharacterLifecycleGuards,
				DomainKeys.CharacterLifecycleGuard(character.Id),
				new CharacterLifecycleGuardRecord { CharacterId = character.Id, ReferenceRevision = 0 });
			unitOfWork.Create(
				environment.Repositories.CharacterSlots,
				DomainKeys.CharacterSlot(account, CharacterRules.MaximumSlots),
				new CharacterSlotRecord
				{
					AccountId = account,
					Slot = CharacterRules.MaximumSlots,
					CharacterId = character.Id
				});
		});
		var report = new DomainInvariantValidator(
			environment.Repositories,
			environment.Schema,
			new SchemaItemShapeCatalog(environment.Schema, environment.Repositories),
			environment.PersistenceProfile).Validate();

		Assert.IsFalse(report.IsValid);
		Assert.IsTrue(report.Issues.Any(issue =>
			issue.Path.StartsWith("character-slot/", StringComparison.Ordinal) &&
			issue.Message.Contains("outside 0..", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue =>
			issue.Path == $"character/{character.Id}" &&
			issue.Message.Contains("outside 0..", StringComparison.Ordinal)));
	}

	[TestMethod]
	public async Task DuplicateLogicalAggregateIdsUnderForgedOuterKeysAreReportedWithoutThrowing()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var item = ApplicationServiceTestEnvironment.Item();
		var inventory = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.SceneEntity(SceneEntityId.New()),
			new[] { new InventoryPlacement(item.Id, 0, 0) });
		await environment.SeedAsync(unit =>
		{
			unit.Create(environment.Repositories.Items, DomainKeys.Item(item.Id), item);
			unit.Create(environment.Repositories.Items, "forged-item-key", item);
			unit.Create(environment.Repositories.Inventories, DomainKeys.Inventory(inventory.Id), inventory);
			unit.Create(environment.Repositories.Inventories, "forged-inventory-key", inventory);
		});
		var report = new DomainInvariantValidator(
			environment.Repositories,
			environment.Schema,
			new SchemaItemShapeCatalog(environment.Schema, environment.Repositories),
			environment.PersistenceProfile).Validate();

		Assert.IsFalse(report.IsValid);
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("Logical item ID is duplicated", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("Logical inventory ID is duplicated", StringComparison.Ordinal)));
	}

	[TestMethod]
	public async Task CorruptCrossDocumentGraphReportsEachInvariantClass()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var character = ApplicationServiceTestEnvironment.Character(new AccountId(9002), 0);
		var outOfBounds = ApplicationServiceTestEnvironment.Item();
		var overlap = ApplicationServiceTestEnvironment.Item();
		var missingId = ItemId.New();
		var firstBag = ApplicationServiceTestEnvironment.Item(ApplicationServiceTestEnvironment.BagDefinitionId);
		var secondBag = ApplicationServiceTestEnvironment.Item(ApplicationServiceTestEnvironment.BagDefinitionId);
		var corruptInventory = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.SceneEntity(SceneEntityId.New()),
			new[]
			{
				new InventoryPlacement(outOfBounds.Id, 1, 0),
				new InventoryPlacement(overlap.Id, 0, 0),
				new InventoryPlacement(missingId, 0, 0)
			},
			width: 1,
			height: 1);
		var invalidDimensions = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.SceneEntity(SceneEntityId.New()),
			width: 0,
			height: 1);
		var firstBagInventory = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.ParentItem(firstBag.Id),
			new[] { new InventoryPlacement(secondBag.Id, 0, 0) });
		var secondBagInventory = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.ParentItem(secondBag.Id),
			new[] { new InventoryPlacement(firstBag.Id, 0, 0) });
		await environment.SeedAsync(unitOfWork =>
		{
			unitOfWork.Create(environment.Repositories.Characters, DomainKeys.Character(character.Id), character);
			foreach (var item in new[] { outOfBounds, overlap, firstBag, secondBag })
				unitOfWork.Create(environment.Repositories.Items, DomainKeys.Item(item.Id), item);
			foreach (var inventory in new[]
				{ corruptInventory, invalidDimensions, firstBagInventory, secondBagInventory })
				unitOfWork.Create(environment.Repositories.Inventories, DomainKeys.Inventory(inventory.Id), inventory);
			unitOfWork.Create(
				environment.Repositories.WorldItems,
				DomainKeys.WorldItem(outOfBounds.Id),
				new WorldItemRecord
				{
					ItemId = outOfBounds.Id,
					Transform = ApplicationServiceTestEnvironment.Transform()
				});
		});
		var validator = new DomainInvariantValidator(
			environment.Repositories,
			environment.Schema,
			new SchemaItemShapeCatalog(environment.Schema, environment.Repositories),
			environment.PersistenceProfile);

		var report = validator.Validate();

		Assert.IsFalse(report.IsValid);
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("owner-slot", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("one main inventory", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("dimensions", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("missing item", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("exceeds inventory bounds", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("overlap", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("exactly one location", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("cycle", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Code == ErrorCode.NotFound));
		Assert.IsTrue(report.Issues.Any(issue => issue.Code == ErrorCode.InvalidArgument));
		Assert.IsTrue(report.Issues.Any(issue => issue.Code == ErrorCode.Conflict));
	}

	[TestMethod]
	public async Task NestedPayloadKindsVersionsCodecsAndOuterKeysAreValidated()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId(9003);
		var character = ApplicationServiceTestEnvironment.Character(account, 0) with
		{
			SchemaState = ApplicationServiceTestEnvironment.StatePayload(version: 2)
		};
		var item = ApplicationServiceTestEnvironment.Item() with
		{
			Traits = new Dictionary<string, TypedPayload>(StringComparer.Ordinal)
			{
				["state"] = new TypedPayload
				{
					TypeId = new PersistedTypeId(ApplicationServiceTestEnvironment.StateTypeId),
					TypeVersion = 1,
					Data = System.Text.Json.JsonSerializer.SerializeToElement("malformed")
				}
			}
		};
		var inventory = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(character.Id),
			new[] { new InventoryPlacement(item.Id, 0, 0) });
		var reference = new CharacterReferenceRecord
		{
			Category = "unknown_reference",
			CharacterId = character.Id,
			State = ApplicationServiceTestEnvironment.StatePayload()
		};
		var sceneEntity = new PersistentSceneEntityRecord
		{
			Id = SceneEntityId.New(),
			Kind = "storage",
			State = ApplicationServiceTestEnvironment.StatePayload(typeId: "unknown.state")
		};
		await environment.SeedAsync(unitOfWork =>
		{
			unitOfWork.Create(environment.Repositories.Characters, "wrong-character-key", character);
			unitOfWork.Create(
				environment.Repositories.CharacterSlots,
				DomainKeys.CharacterSlot(account, 0),
				new CharacterSlotRecord { AccountId = account, Slot = 0, CharacterId = character.Id });
			unitOfWork.Create(environment.Repositories.Items, DomainKeys.Item(item.Id), item);
			unitOfWork.Create(environment.Repositories.Inventories, DomainKeys.Inventory(inventory.Id), inventory);
			unitOfWork.Create(
				environment.Repositories.OwnerInventories,
				DomainKeys.OwnerInventory(inventory.Owner, "main"),
				new OwnerInventoryRecord { Owner = inventory.Owner, Role = "main", InventoryId = inventory.Id });
			unitOfWork.Create(environment.Repositories.CharacterReferences, "reference", reference);
			unitOfWork.Create(environment.Repositories.SceneEntities, DomainKeys.SceneEntity(sceneEntity.Id), sceneEntity);
		});
		var profile = new SchemaPersistenceInvariantProfile(
			new PersistedTypeId(ApplicationServiceTestEnvironment.StateTypeId),
			new[]
			{
				ItemPersistenceContract.WithTrait(
					ApplicationServiceTestEnvironment.ItemDefinitionId,
					"state",
					ApplicationServiceTestEnvironment.StateTypeId),
				ItemPersistenceContract.WithoutTraits(ApplicationServiceTestEnvironment.BagDefinitionId)
			},
			new Dictionary<string, PersistedTypeId>(StringComparer.Ordinal)
			{
				["recognition"] = new PersistedTypeId(ApplicationServiceTestEnvironment.StateTypeId)
			},
			new Dictionary<string, PersistedTypeId>(StringComparer.Ordinal)
			{
				["storage"] = new PersistedTypeId(ApplicationServiceTestEnvironment.StateTypeId)
			});
		var report = new DomainInvariantValidator(
			environment.Repositories,
			environment.Schema,
			new SchemaItemShapeCatalog(environment.Schema, environment.Repositories),
			profile).Validate();

		Assert.IsFalse(report.IsValid);
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("not canonical", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Path.EndsWith("schema-state", StringComparison.Ordinal) &&
			issue.Message.Contains("incompatible", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Path.Contains("traits/state", StringComparison.Ordinal) &&
			issue.Message.Contains("could not be decoded", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("not declared", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Path.Contains("scene-entity", StringComparison.Ordinal) &&
			issue.Message.Contains("Expected persisted type", StringComparison.Ordinal)));
	}

	[TestMethod]
	public async Task CanonicalOwnerIndexesRejectMissingExtraAndWrongMainOwnership()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId(9004);
		var character = ApplicationServiceTestEnvironment.Character(account, 0);
		var main = ApplicationServiceTestEnvironment.Inventory(InventoryOwner.Character(character.Id));
		var extra = ApplicationServiceTestEnvironment.Inventory(InventoryOwner.Character(character.Id));
		await environment.SeedAsync(unitOfWork =>
		{
			unitOfWork.Create(environment.Repositories.Characters, DomainKeys.Character(character.Id), character);
			unitOfWork.Create(
				environment.Repositories.CharacterSlots,
				DomainKeys.CharacterSlot(account, 0),
				new CharacterSlotRecord { AccountId = account, Slot = 0, CharacterId = character.Id });
			unitOfWork.Create(environment.Repositories.Inventories, DomainKeys.Inventory(main.Id), main);
			unitOfWork.Create(environment.Repositories.Inventories, DomainKeys.Inventory(extra.Id), extra);
			unitOfWork.Create(
				environment.Repositories.OwnerInventories,
				"forged-index-key",
				new OwnerInventoryRecord { Owner = main.Owner, Role = "storage", InventoryId = main.Id });
			unitOfWork.Create(
				environment.Repositories.OwnerInventories,
				DomainKeys.OwnerInventory(InventoryOwner.SceneEntity(SceneEntityId.New()), "storage"),
				new OwnerInventoryRecord
				{
					Owner = InventoryOwner.SceneEntity(SceneEntityId.New()),
					Role = "storage",
					InventoryId = InventoryId.New()
				});
		});
		var report = new DomainInvariantValidator(
			environment.Repositories,
			environment.Schema,
			new SchemaItemShapeCatalog(environment.Schema, environment.Repositories),
			environment.PersistenceProfile).Validate();

		Assert.IsFalse(report.IsValid);
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("not canonical", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("requires role 'main'", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("missing inventory", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("canonical owner index, found 0", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("Expected one main inventory and canonical index", StringComparison.Ordinal)));
	}

	[TestMethod]
	public async Task RecoveredOuterEnvelopeWithMalformedNestedStateFailsNeutralValidation()
	{
		var storage = new InMemoryPersistenceStorage();
		var schemaResult = SchemaCompiler.Compile(new RecoverySchema());
		Assert.IsTrue(schemaResult.Succeeded, schemaResult.Error?.Message);
		var schema = schemaResult.Value;
		var characterId = CharacterId.New();
		var account = new AccountId(9005);
		var inventory = ApplicationServiceTestEnvironment.Inventory(InventoryOwner.Character(characterId));
		var registry = RecoveryRegistry();
		await using (var first = RecoveryProvider(storage, registry))
		{
			await first.InitializeAsync();
			var repositories = new DomainRepositories(first);
			var malformed = ApplicationServiceTestEnvironment.Character(account, 0, characterId) with
			{
				SchemaState = new TypedPayload
				{
					TypeId = new PersistedTypeId(ApplicationServiceTestEnvironment.StateTypeId),
					TypeVersion = 1,
					Data = System.Text.Json.JsonSerializer.SerializeToElement("not-an-object")
				}
			};
			await using var unit = first.BeginUnitOfWork();
			unit.Create(repositories.Characters, DomainKeys.Character(characterId), malformed);
			unit.Create(repositories.CharacterSlots, DomainKeys.CharacterSlot(account, 0),
				new CharacterSlotRecord { AccountId = account, Slot = 0, CharacterId = characterId });
			unit.Create(repositories.Inventories, DomainKeys.Inventory(inventory.Id), inventory);
			unit.Create(repositories.OwnerInventories, DomainKeys.OwnerInventory(inventory.Owner, "main"),
				new OwnerInventoryRecord { Owner = inventory.Owner, Role = "main", InventoryId = inventory.Id });
			Assert.IsTrue((await unit.CommitAsync()).Succeeded);
			Assert.IsTrue( (await first.ShutdownAsync()).IsClean );
		}

		await using var recovered = RecoveryProvider(storage, RecoveryRegistry());
		await recovered.InitializeAsync();
		var recoveredRepositories = new DomainRepositories(recovered);
		var profile = new SchemaPersistenceInvariantProfile(
			new PersistedTypeId(ApplicationServiceTestEnvironment.StateTypeId),
			new[] { ItemPersistenceContract.WithoutTraits(ApplicationServiceTestEnvironment.ItemDefinitionId) },
			Array.Empty<KeyValuePair<string, PersistedTypeId>>(),
			Array.Empty<KeyValuePair<string, PersistedTypeId>>());
		var report = new DomainInvariantValidator(
			recoveredRepositories,
			schema,
			new SchemaItemShapeCatalog(schema, recoveredRepositories),
			profile).Validate();

		Assert.IsFalse(report.IsValid);
		Assert.IsTrue(report.Issues.Any(issue => issue.Path.EndsWith("schema-state", StringComparison.Ordinal) &&
			issue.Message.Contains("could not be decoded", StringComparison.Ordinal)));
	}

	[TestMethod]
	public async Task NonCanonicalNestedPayloadFailsCodecRoundTripInvariant()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId(9009);
		var character = ApplicationServiceTestEnvironment.Character(account, 0) with
		{
			SchemaState = new TypedPayload
			{
				TypeId = new PersistedTypeId(ApplicationServiceTestEnvironment.StateTypeId),
				TypeVersion = 1,
				Data = System.Text.Json.JsonDocument.Parse("{ \"Name\" : \"test\" }").RootElement.Clone()
			}
		};
		var inventory = ApplicationServiceTestEnvironment.Inventory(InventoryOwner.Character(character.Id));
		await environment.SeedAsync(unit =>
		{
			unit.Create(environment.Repositories.Characters, DomainKeys.Character(character.Id), character);
			unit.Create(environment.Repositories.CharacterSlots, DomainKeys.CharacterSlot(account, 0),
				new CharacterSlotRecord { AccountId = account, Slot = 0, CharacterId = character.Id });
			unit.Create(environment.Repositories.CharacterLifecycleGuards, DomainKeys.CharacterLifecycleGuard(character.Id),
				new CharacterLifecycleGuardRecord { CharacterId = character.Id, ReferenceRevision = 0 });
			unit.Create(environment.Repositories.Inventories, DomainKeys.Inventory(inventory.Id), inventory);
			unit.Create(environment.Repositories.OwnerInventories, DomainKeys.OwnerInventory(inventory.Owner, "main"),
				new OwnerInventoryRecord { Owner = inventory.Owner, Role = "main", InventoryId = inventory.Id });
		});

		var report = new DomainInvariantValidator(
			environment.Repositories,
			environment.Schema,
			new SchemaItemShapeCatalog(environment.Schema, environment.Repositories),
			environment.PersistenceProfile).Validate();

		Assert.IsFalse(report.IsValid);
		Assert.IsTrue(report.Issues.Any(issue =>
			issue.Path.EndsWith("schema-state", StringComparison.Ordinal) &&
			issue.Code == ErrorCode.PersistedTypeInvalid));
	}

	[TestMethod]
	public async Task EquivalentJsonStringEscapesPassCodecRoundTripInvariant()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId( 9011 );
		var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
		{
			SchemaState = new TypedPayload
			{
				TypeId = new PersistedTypeId( ApplicationServiceTestEnvironment.StateTypeId ),
				TypeVersion = 1,
				Data = System.Text.Json.JsonDocument.Parse( "{\"name\":\"A\\u002BB\"}" ).RootElement.Clone()
			}
		};
		var inventory = ApplicationServiceTestEnvironment.Inventory( InventoryOwner.Character( character.Id ) );
		await environment.SeedAsync( unit =>
		{
			unit.Create( environment.Repositories.Characters, DomainKeys.Character( character.Id ), character );
			unit.Create( environment.Repositories.CharacterSlots, DomainKeys.CharacterSlot( account, 0 ),
				new CharacterSlotRecord { AccountId = account, Slot = 0, CharacterId = character.Id } );
			unit.Create( environment.Repositories.CharacterLifecycleGuards, DomainKeys.CharacterLifecycleGuard( character.Id ),
				new CharacterLifecycleGuardRecord { CharacterId = character.Id, ReferenceRevision = 0 } );
			unit.Create( environment.Repositories.Inventories, DomainKeys.Inventory( inventory.Id ), inventory );
			unit.Create( environment.Repositories.OwnerInventories, DomainKeys.OwnerInventory( inventory.Owner, "main" ),
				new OwnerInventoryRecord { Owner = inventory.Owner, Role = "main", InventoryId = inventory.Id } );
		} );

		var report = new DomainInvariantValidator(
			environment.Repositories,
			environment.Schema,
			new SchemaItemShapeCatalog( environment.Schema, environment.Repositories ),
			environment.PersistenceProfile ).Validate();

		Assert.IsTrue( report.IsValid, string.Join( Environment.NewLine, report.Issues.Select( issue => issue.Message ) ) );
	}

	[TestMethod]
	public async Task RecoveryRejectsIntegerExtremeInventoryGeometryBeforeReady()
	{
		var storage = new InMemoryPersistenceStorage();
		var schemaResult = SchemaCompiler.Compile(new RecoverySchema());
		Assert.IsTrue(schemaResult.Succeeded, schemaResult.Error?.Message);
		var schema = schemaResult.Value;
		var characterId = CharacterId.New();
		var account = new AccountId(9010);
		var character = ApplicationServiceTestEnvironment.Character(account, 0, characterId);
		var item = ApplicationServiceTestEnvironment.Item();
		var inventory = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(characterId),
			new[] { new InventoryPlacement(item.Id, int.MaxValue, int.MaxValue) });
		await using (var writer = RecoveryProvider(storage, RecoveryRegistry()))
		{
			await writer.InitializeAsync();
			var repositories = new DomainRepositories(writer);
			await using var unit = writer.BeginUnitOfWork();
			unit.Create(repositories.Characters, DomainKeys.Character(characterId), character);
			unit.Create(repositories.CharacterSlots, DomainKeys.CharacterSlot(account, 0),
				new CharacterSlotRecord { AccountId = account, Slot = 0, CharacterId = characterId });
			unit.Create(repositories.CharacterLifecycleGuards, DomainKeys.CharacterLifecycleGuard(characterId),
				new CharacterLifecycleGuardRecord { CharacterId = characterId, ReferenceRevision = 0 });
			unit.Create(repositories.Items, DomainKeys.Item(item.Id), item);
			unit.Create(repositories.Inventories, DomainKeys.Inventory(inventory.Id), inventory);
			unit.Create(repositories.OwnerInventories, DomainKeys.OwnerInventory(inventory.Owner, "main"),
				new OwnerInventoryRecord { Owner = inventory.Owner, Role = "main", InventoryId = inventory.Id });
			Assert.IsTrue((await unit.CommitAsync()).Succeeded);
			Assert.IsTrue((await writer.ShutdownAsync()).IsClean);
		}

		var readerTypes = RecoveryRegistry();
		var profile = new SchemaPersistenceInvariantProfile(
			new PersistedTypeId(ApplicationServiceTestEnvironment.StateTypeId),
			new[] { ItemPersistenceContract.WithoutTraits(ApplicationServiceTestEnvironment.ItemDefinitionId) },
			Array.Empty<KeyValuePair<string, PersistedTypeId>>(),
			Array.Empty<KeyValuePair<string, PersistedTypeId>>());
		var invariants = new DomainInvariantValidator(
			readerTypes, schema, new SchemaItemShapeCatalog(schema), profile);
		await using var recovered = new FileSystemPersistenceProvider(
			storage,
			new FileSystemPersistenceOptions("outer-envelope-test") { CheckpointEveryCommits = 0 },
			readerTypes,
			invariants);

		await Assert.ThrowsAsync<PersistenceCorruptionException>(async () => await recovered.InitializeAsync());
		Assert.AreEqual(PersistenceProviderState.Faulted, recovered.State);
	}

	[TestMethod]
	public async Task ReverseGuardsReservationsAndReferenceTargetsFailClosed()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId(9006);
		var character = ApplicationServiceTestEnvironment.Character(account, 0);
		var inventory = ApplicationServiceTestEnvironment.Inventory(InventoryOwner.Character(character.Id));
		var missingCharacter = CharacterId.New();
		var missingRelated = CharacterId.New();
		var missingScene = SceneEntityId.New();
		await environment.SeedAsync(unit =>
		{
			unit.Create(environment.Repositories.Characters, DomainKeys.Character(character.Id), character);
			unit.Create(environment.Repositories.CharacterSlots, DomainKeys.CharacterSlot(account, 0),
				new CharacterSlotRecord { AccountId = account, Slot = 0, CharacterId = character.Id });
			unit.Create(environment.Repositories.CharacterSlots, "duplicate-slot-key",
				new CharacterSlotRecord { AccountId = account, Slot = 0, CharacterId = character.Id });
			unit.Create(environment.Repositories.CharacterSlots, DomainKeys.CharacterSlot(new AccountId(9007), 1),
				new CharacterSlotRecord { AccountId = new AccountId(9007), Slot = 1, CharacterId = missingCharacter });
			unit.Create(environment.Repositories.Inventories, DomainKeys.Inventory(inventory.Id), inventory);
			unit.Create(environment.Repositories.OwnerInventories, DomainKeys.OwnerInventory(inventory.Owner, "main"),
				new OwnerInventoryRecord { Owner = inventory.Owner, Role = "main", InventoryId = inventory.Id });
			unit.Create(environment.Repositories.UniqueReservations, "forged-reservation-key",
				new UniqueReservationRecord { Namespace = "test.cid", Value = "12345", CharacterId = character.Id });
			unit.Create(environment.Repositories.UniqueReservations, DomainKeys.UniqueReservation("test.cid", "12345"),
				new UniqueReservationRecord { Namespace = "test.cid", Value = "12345", CharacterId = missingCharacter });
			unit.Create(environment.Repositories.CharacterReferences, "strict-reference", new CharacterReferenceRecord
			{
				Category = "strict",
				CharacterId = missingCharacter,
				RelatedCharacterId = missingRelated,
				SceneEntityId = missingScene,
				State = ApplicationServiceTestEnvironment.StatePayload()
			});
			unit.Create(environment.Repositories.CharacterReferences, "external-reference", new CharacterReferenceRecord
			{
				Category = "external",
				CharacterId = missingCharacter,
				RelatedCharacterId = missingRelated,
				SceneEntityId = missingScene,
				State = ApplicationServiceTestEnvironment.StatePayload()
			});
		});
		var profile = new SchemaPersistenceInvariantProfile(
			new PersistedTypeId(ApplicationServiceTestEnvironment.StateTypeId),
			new[]
			{
				ItemPersistenceContract.WithoutTraits(ApplicationServiceTestEnvironment.ItemDefinitionId),
				ItemPersistenceContract.WithoutTraits(ApplicationServiceTestEnvironment.BagDefinitionId)
			},
			new[]
			{
				new CharacterReferencePersistenceContract(
					"strict", new PersistedTypeId(ApplicationServiceTestEnvironment.StateTypeId)),
				new CharacterReferencePersistenceContract(
					"external", new PersistedTypeId(ApplicationServiceTestEnvironment.StateTypeId), true, true, true)
			},
			Array.Empty<KeyValuePair<string, PersistedTypeId>>());
		var report = new DomainInvariantValidator(
			environment.Repositories,
			environment.Schema,
			new SchemaItemShapeCatalog(environment.Schema, environment.Repositories),
			profile).Validate();

		Assert.IsFalse(report.IsValid);
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("Logical owner-slot guard is duplicated", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("Owner-slot guard references 0 characters", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("Logical unique reservation is duplicated", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Message.Contains("Unique reservation references a missing character", StringComparison.Ordinal)));
		Assert.IsTrue(report.Issues.Any(issue => issue.Path == "character-reference/strict-reference/character"));
		Assert.IsTrue(report.Issues.Any(issue => issue.Path == "character-reference/strict-reference/related-character"));
		Assert.IsTrue(report.Issues.Any(issue => issue.Path == "character-reference/strict-reference/scene-entity"));
		Assert.IsFalse(report.Issues.Any(issue => issue.Path.StartsWith("character-reference/external-reference/", StringComparison.Ordinal) &&
			!issue.Path.EndsWith("/state", StringComparison.Ordinal)));
	}

	[TestMethod]
	[Timeout(30_000, CooperativeCancellation = true)]
	public async Task TenThousandPlacementsUseBoundedOverlapValidation()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		const int itemCount = 10_000;
		var documents = new Dictionary<DocumentAddress, PersistenceCandidateDocument>();
		var itemCodec = environment.Provider.Types.Resolve<ItemRecord>();
		var inventoryCodec = environment.Provider.Types.Resolve<InventoryRecord>();
		var placements = new InventoryPlacement[itemCount];
		for ( var index = 0; index < itemCount; index++ )
		{
			var item = ApplicationServiceTestEnvironment.Item(
				id: new ItemId( IndexedGuid( index, 1 ) ) );
			var address = new DocumentAddress( DomainCollections.Items, DomainKeys.Item( item.Id ) );
			documents.Add( address, new PersistenceCandidateDocument(
				address, new DocumentRevision( 1 ), itemCodec.Key, itemCodec.CurrentVersion, item ) );
			placements[index] = new InventoryPlacement(
				item.Id,
				index == itemCount - 1 ? itemCount - 2 : index,
				0 );
		}
		var inventory = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.SceneEntity( new SceneEntityId( IndexedGuid( 1, 2 ) ) ),
			placements,
			width: itemCount,
			height: 1,
			id: new InventoryId( IndexedGuid( 1, 3 ) ) );
		var inventoryAddress = new DocumentAddress(
			DomainCollections.Inventories, DomainKeys.Inventory( inventory.Id ) );
		documents.Add( inventoryAddress, new PersistenceCandidateDocument(
			inventoryAddress,
			new DocumentRevision( 1 ),
			inventoryCodec.Key,
			inventoryCodec.CurrentVersion,
			inventory ) );

		var issues = new DomainInvariantValidator(
			environment.Provider.Types,
			environment.Schema,
			new SchemaItemShapeCatalog( environment.Schema ),
			environment.PersistenceProfile ).Validate(
				new PersistenceInvariantContext( 1, 0, documents, isRecovery: true ) );

		Assert.IsTrue( issues.Any( issue => issue.Message.Contains( "overlap", StringComparison.Ordinal ) ) );
	}

	[TestMethod]
	[Timeout(30_000, CooperativeCancellation = true)]
	public async Task TenThousandInventoryOwnershipChainUsesLinearCycleValidation()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		const int inventoryCount = 10_000;
		var documents = new Dictionary<DocumentAddress, PersistenceCandidateDocument>();
		var inventoryCodec = environment.Provider.Types.Resolve<InventoryRecord>();
		var parentItems = Enumerable.Range( 0, inventoryCount )
			.Select( index => new ItemId( IndexedGuid( index, 4 ) ) )
			.ToArray();
		for ( var index = 0; index < inventoryCount; index++ )
		{
			var inventory = ApplicationServiceTestEnvironment.Inventory(
				InventoryOwner.ParentItem( parentItems[index] ),
				index == 0
					? Array.Empty<InventoryPlacement>()
					: new[] { new InventoryPlacement( parentItems[index - 1], 0, 0 ) },
				width: 1,
				height: 1,
				id: new InventoryId( IndexedGuid( index, 5 ) ) );
			var address = new DocumentAddress(
				DomainCollections.Inventories, DomainKeys.Inventory( inventory.Id ) );
			documents.Add( address, new PersistenceCandidateDocument(
				address,
				new DocumentRevision( 1 ),
				inventoryCodec.Key,
				inventoryCodec.CurrentVersion,
				inventory ) );
		}

		var issues = new DomainInvariantValidator(
			environment.Provider.Types,
			environment.Schema,
			new SchemaItemShapeCatalog( environment.Schema ),
			environment.PersistenceProfile ).Validate(
				new PersistenceInvariantContext( 1, 0, documents, isRecovery: true ) );

		Assert.IsFalse( issues.Any( issue => issue.Message.Contains( "cycle", StringComparison.Ordinal ) ) );
	}

	[TestMethod]
	public async Task CommitValidationRoundTripsOnlyChangedPayloadsWhileRecoveryChecksAllPayloads()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var unchanged = ApplicationServiceTestEnvironment.Character( new AccountId( 9101 ), 0 );
		unchanged = unchanged with
		{
			SchemaState = unchanged.SchemaState with
			{
				Data = JsonSerializer.SerializeToElement( new { name = 42 } )
			}
		};
		var changed = ApplicationServiceTestEnvironment.Character( new AccountId( 9102 ), 0 );
		var codec = environment.Provider.Types.Resolve<CharacterRecord>();
		var unchangedAddress = new DocumentAddress(
			DomainCollections.Characters, DomainKeys.Character( unchanged.Id ) );
		var changedAddress = new DocumentAddress(
			DomainCollections.Characters, DomainKeys.Character( changed.Id ) );
		var documents = new Dictionary<DocumentAddress, PersistenceCandidateDocument>
		{
			[unchangedAddress] = new(
				unchangedAddress, new DocumentRevision( 1 ), codec.Key, codec.CurrentVersion, unchanged ),
			[changedAddress] = new(
				changedAddress, new DocumentRevision( 1 ), codec.Key, codec.CurrentVersion, changed )
		};
		var validator = new DomainInvariantValidator(
			environment.Provider.Types,
			environment.Schema,
			new SchemaItemShapeCatalog( environment.Schema ),
			environment.PersistenceProfile );

		var commitIssues = validator.Validate( new PersistenceInvariantContext(
			2,
			0,
			documents,
			previousDocuments: documents,
			changedAddresses: new HashSet<DocumentAddress> { changedAddress } ) );
		var recoveryIssues = validator.Validate( new PersistenceInvariantContext(
			2, 0, documents, isRecovery: true ) );

		Assert.IsFalse( commitIssues.Any( issue => issue.Path == $"character/{unchangedAddress.Key}/schema-state" ) );
		Assert.IsTrue( recoveryIssues.Any( issue => issue.Path == $"character/{unchangedAddress.Key}/schema-state" ) );
	}

	[TestMethod]
	public async Task IncrementalValidationMatchesFullValidationForChangedDependencyClosure()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var documents = BuildIndependentCharacterGraphs( environment, 200 );
		var validator = IncrementalValidator( environment );
		var recovery = new PersistenceInvariantContext( 1, 0, documents, isRecovery: true );
		Assert.IsEmpty( validator.Validate( recovery ) );
		validator.Rebuild( recovery );

		var changedAddress = documents.Keys.First( address => address.Collection == DomainCollections.Characters );
		var previous = documents[changedAddress];
		var character = (CharacterRecord)previous.Value;
		var changedDocument = previous with
		{
			Revision = new DocumentRevision( 2 ),
			Value = character with { AccountId = new AccountId( character.AccountId.Value + 100_000 ) }
		};
		var changed = new HashSet<DocumentAddress> { changedAddress };
		var delta = new Dictionary<DocumentAddress, PersistenceCandidateDocument?>
		{
			[changedAddress] = changedDocument
		};
		var incrementalContext = new PersistenceInvariantContext(
			2, 0, new PersistenceInvariantDocumentIndex( documents.Values ), delta, changed );
		var incremental = validator.Prepare( incrementalContext );

		var fullDocuments = new Dictionary<DocumentAddress, PersistenceCandidateDocument>( documents )
		{
			[changedAddress] = changedDocument
		};
		var full = validator.Validate( new PersistenceInvariantContext(
			2, 0, fullDocuments, documents, changed ) );

		AssertIssueSetsEqual( full, incremental.Issues );
		Assert.IsNotEmpty( full );
	}

	[TestMethod]
	[DataRow( 1_000 )]
	[DataRow( 10_000 )]
	[DataRow( 100_000 )]
	[Timeout( 30_000, CooperativeCancellation = true )]
	public async Task IncrementalValidationWorkRemainsBoundedAcrossStoreSizes( int documentCount )
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		const int documentsPerGraph = 5;
		Assert.AreEqual( 0, documentCount % documentsPerGraph );
		var graphCount = documentCount / documentsPerGraph;
		var documents = BuildIndependentCharacterGraphs( environment, graphCount );
		var validator = IncrementalValidator( environment );
		var recovery = new PersistenceInvariantContext( 1, 0, documents, isRecovery: true );
		Assert.IsEmpty( validator.Validate( recovery ) );
		validator.Rebuild( recovery );
		Assert.AreEqual( documentCount, validator.IndexedDocumentCount );

		var changedAddress = documents.Keys.First( address => address.Collection == DomainCollections.Characters );
		var previous = documents[changedAddress];
		var character = (CharacterRecord)previous.Value;
		var changedDocument = previous with
		{
			Revision = new DocumentRevision( 2 ),
			Value = character with { Name = character.Name + " updated" }
		};
		var changed = new HashSet<DocumentAddress> { changedAddress };
		var delta = new Dictionary<DocumentAddress, PersistenceCandidateDocument?>
		{
			[changedAddress] = changedDocument
		};
		var context = new PersistenceInvariantContext(
			2, 0, new PersistenceInvariantDocumentIndex( documents.Values ), delta, changed );

		var prepared = validator.Prepare( context );

		Assert.IsEmpty( prepared.Issues );
		Assert.IsLessThanOrEqualTo(
			16L,
			context.VisitedDocumentCount,
			$"One-record validation visited {context.VisitedDocumentCount} candidate documents in a {documentCount}-document store." );
		validator.Publish( prepared );
		Assert.AreEqual( documentCount, validator.IndexedDocumentCount );
	}

	[TestMethod]
	public async Task RandomizedIncrementalMutationsMatchFullValidation()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		const int graphCount = 128;
		const int mutationKindCount = 8;
		const int mutationCount = 48;
		var documents = BuildIndependentCharacterGraphs( environment, graphCount );
		var baseIndex = new PersistenceInvariantDocumentIndex( documents.Values );
		var validator = IncrementalValidator( environment );
		var recovery = new PersistenceInvariantContext( 1, 0, documents, isRecovery: true );
		Assert.IsEmpty( validator.Validate( recovery ) );
		validator.Rebuild( recovery );
		var random = new Random( 0x5EED_2026 );
		var coveredMutationKinds = new bool[mutationKindCount];

		for ( var iteration = 0; iteration < mutationCount; iteration++ )
		{
			var graphIndex = random.Next( graphCount );
			var mutationKind = iteration < mutationKindCount ? iteration : random.Next( mutationKindCount );
			coveredMutationKinds[mutationKind] = true;
			var addresses = IndependentGraphAddresses( graphIndex );
			var delta = new Dictionary<DocumentAddress, PersistenceCandidateDocument?>();
			switch ( mutationKind )
			{
				case 0:
				{
					var document = documents[addresses.Character];
					var character = (CharacterRecord)document.Value;
					delta.Add( addresses.Character, ReplaceValue(
						document,
						character with { Name = $"{character.Name}-{random.Next():x8}" } ) );
					break;
				}
				case 1:
				{
					var document = documents[addresses.Character];
					var character = (CharacterRecord)document.Value;
					delta.Add( addresses.Character, ReplaceValue(
						document,
						character with { AccountId = new AccountId( (ulong)(1_000_000 + random.Next( 1_000_000 )) ) } ) );
					break;
				}
				case 2:
					delta.Add( addresses.Slot, null );
					break;
				case 3:
				{
					var document = documents[addresses.Guard];
					var guard = (CharacterLifecycleGuardRecord)document.Value;
					delta.Add( addresses.Guard, ReplaceValue(
						document,
						guard with { CharacterId = new CharacterId( IndexedGuid( graphIndex, 12 ) ) } ) );
					break;
				}
				case 4:
				{
					var document = documents[addresses.OwnerInventory];
					var ownerInventory = (OwnerInventoryRecord)document.Value;
					delta.Add( addresses.OwnerInventory, ReplaceValue(
						document,
						ownerInventory with { InventoryId = new InventoryId( IndexedGuid( graphIndex, 13 ) ) } ) );
					break;
				}
				case 5:
				{
					var document = documents[addresses.Inventory];
					var inventory = (InventoryRecord)document.Value;
					var otherCharacter = new CharacterId( IndexedGuid( (graphIndex + 1) % graphCount, 10 ) );
					delta.Add( addresses.Inventory, ReplaceValue(
						document,
						inventory with { Owner = InventoryOwner.Character( otherCharacter ) } ) );
					break;
				}
				case 6:
				{
					var source = documents[addresses.Character];
					var forgedAddress = new DocumentAddress(
						DomainCollections.Characters, $"forged-{iteration}-{random.Next():x8}" );
					delta.Add( forgedAddress, source with
					{
						Address = forgedAddress,
						Revision = new DocumentRevision( 1 )
					} );
					break;
				}
				case 7:
					delta.Add( addresses.Character, null );
					delta.Add( addresses.Slot, null );
					delta.Add( addresses.Guard, null );
					delta.Add( addresses.Inventory, null );
					delta.Add( addresses.OwnerInventory, null );
					break;
				default:
					Assert.Fail( $"Unknown mutation kind {mutationKind}." );
					break;
			}

			AssertIncrementalMatchesFull( validator, baseIndex, documents, delta, iteration + 2 );
		}

		Assert.IsTrue( coveredMutationKinds.All( covered => covered ) );
	}

	private static DomainInvariantValidator IncrementalValidator( ApplicationServiceTestEnvironment environment ) => new(
		environment.Provider.Types,
		environment.Schema,
		new SchemaItemShapeCatalog( environment.Schema ),
		environment.PersistenceProfile );

	private static Dictionary<DocumentAddress, PersistenceCandidateDocument> BuildIndependentCharacterGraphs(
		ApplicationServiceTestEnvironment environment,
		int count )
	{
		var documents = new Dictionary<DocumentAddress, PersistenceCandidateDocument>();
		for ( var index = 0; index < count; index++ )
		{
			var account = new AccountId( (ulong)(20_000 + index) );
			var character = ApplicationServiceTestEnvironment.Character(
				account, 0, new CharacterId( IndexedGuid( index, 10 ) ) );
			var inventory = ApplicationServiceTestEnvironment.Inventory(
				InventoryOwner.Character( character.Id ),
				id: new InventoryId( IndexedGuid( index, 11 ) ) );
			AddCandidate(
				documents,
				environment.Provider.Types,
				DomainCollections.Characters,
				DomainKeys.Character( character.Id ),
				character );
			AddCandidate(
				documents,
				environment.Provider.Types,
				DomainCollections.CharacterSlots,
				DomainKeys.CharacterSlot( account, 0 ),
				new CharacterSlotRecord { AccountId = account, Slot = 0, CharacterId = character.Id } );
			AddCandidate(
				documents,
				environment.Provider.Types,
				DomainCollections.CharacterLifecycleGuards,
				DomainKeys.CharacterLifecycleGuard( character.Id ),
				new CharacterLifecycleGuardRecord { CharacterId = character.Id, ReferenceRevision = 0 } );
			AddCandidate(
				documents,
				environment.Provider.Types,
				DomainCollections.Inventories,
				DomainKeys.Inventory( inventory.Id ),
				inventory );
			AddCandidate(
				documents,
				environment.Provider.Types,
				DomainCollections.OwnerInventories,
				DomainKeys.OwnerInventory( inventory.Owner, "main" ),
				new OwnerInventoryRecord { Owner = inventory.Owner, Role = "main", InventoryId = inventory.Id } );
		}
		return documents;
	}

	private static void AddCandidate<T>(
		IDictionary<DocumentAddress, PersistenceCandidateDocument> documents,
		PersistedTypeRegistry types,
		string collection,
		string key,
		T value ) where T : class
	{
		var codec = types.Resolve<T>();
		var address = new DocumentAddress( collection, key );
		documents.Add( address, new PersistenceCandidateDocument(
			address, new DocumentRevision( 1 ), codec.Key, codec.CurrentVersion, value ) );
	}

	private static void AssertIssueSetsEqual(
		IReadOnlyList<PersistenceInvariantIssue> expected,
		IReadOnlyList<PersistenceInvariantIssue> actual )
	{
		static string Key( PersistenceInvariantIssue issue ) => $"{issue.Code}|{issue.Path}|{issue.Message}";
		var expectedKeys = expected.Select( Key ).OrderBy( key => key, StringComparer.Ordinal ).ToArray();
		var actualKeys = actual.Select( Key ).OrderBy( key => key, StringComparer.Ordinal ).ToArray();
		CollectionAssert.AreEqual( expectedKeys, actualKeys );
	}

	private static void AssertIncrementalMatchesFull(
		DomainInvariantValidator validator,
		PersistenceInvariantDocumentIndex baseIndex,
		IReadOnlyDictionary<DocumentAddress, PersistenceCandidateDocument> documents,
		IReadOnlyDictionary<DocumentAddress, PersistenceCandidateDocument?> delta,
		long sequence )
	{
		var changed = delta.Keys.ToHashSet();
		var incremental = validator.Prepare( new PersistenceInvariantContext(
			sequence, 0, baseIndex, delta, changed ) );
		var fullDocuments = new Dictionary<DocumentAddress, PersistenceCandidateDocument>( documents );
		foreach ( var change in delta )
		{
			if ( change.Value is null ) fullDocuments.Remove( change.Key );
			else fullDocuments[change.Key] = change.Value;
		}
		var full = validator.Validate( new PersistenceInvariantContext(
			sequence, 0, fullDocuments, documents, changed ) );
		AssertIssueSetsEqual( full, incremental.Issues );
	}

	private static PersistenceCandidateDocument ReplaceValue<T>(
		PersistenceCandidateDocument document,
		T value ) where T : class => document with
	{
		Revision = new DocumentRevision( document.Revision.Value + 1 ),
		Value = value
	};

	private static IndependentGraphDocumentAddresses IndependentGraphAddresses( int index )
	{
		var character = new CharacterId( IndexedGuid( index, 10 ) );
		var inventory = new InventoryId( IndexedGuid( index, 11 ) );
		var owner = InventoryOwner.Character( character );
		return new IndependentGraphDocumentAddresses(
			new DocumentAddress( DomainCollections.Characters, DomainKeys.Character( character ) ),
			new DocumentAddress( DomainCollections.CharacterSlots, DomainKeys.CharacterSlot( new AccountId( (ulong)(20_000 + index) ), 0 ) ),
			new DocumentAddress( DomainCollections.CharacterLifecycleGuards, DomainKeys.CharacterLifecycleGuard( character ) ),
			new DocumentAddress( DomainCollections.Inventories, DomainKeys.Inventory( inventory ) ),
			new DocumentAddress( DomainCollections.OwnerInventories, DomainKeys.OwnerInventory( owner, "main" ) ) );
	}

	private sealed record IndependentGraphDocumentAddresses(
		DocumentAddress Character,
		DocumentAddress Slot,
		DocumentAddress Guard,
		DocumentAddress Inventory,
		DocumentAddress OwnerInventory );

	private static Guid IndexedGuid( int index, byte discriminator )
	{
		var bytes = new byte[16];
		BitConverter.GetBytes( index + 1 ).CopyTo( bytes, 0 );
		bytes[15] = discriminator;
		return new Guid( bytes );
	}

	private static PersistedTypeRegistry RecoveryRegistry() => new PersistedTypeRegistry()
		.RegisterHexagonDomainTypes()
		.Register<TestCharacterState>(
			new PersistedTypeKey(ApplicationServiceTestEnvironment.StateTypeId),
			1,
			PersistedValuePublication.Immutable);

	private static FileSystemPersistenceProvider RecoveryProvider(
		IPersistenceStorage storage,
		PersistedTypeRegistry registry) => new(
		storage,
		new FileSystemPersistenceOptions("outer-envelope-test"),
		registry);

	private sealed class RecoverySchema : IHexSchema
	{
		public string Id => "outer_envelope_test";

		public void Configure(SchemaBuilder builder)
		{
			builder.RegisterItem(new ItemDefinition(
				ApplicationServiceTestEnvironment.ItemDefinitionId,
				Array.Empty<string>()));
			builder.RegisterPersistedType(new PersistedTypeRegistration(
				ApplicationServiceTestEnvironment.StateTypeId,
				typeof(TestCharacterState),
				1));
		}
	}
}
#nullable enable

using Hexagon.V2.Application;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;

namespace Hexagon.V2.Tests.Application;

[TestClass]
public sealed class InventoryMutationServiceTests
{
	[TestMethod]
	public async Task MoveCommitsBothInventorySnapshots()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var actor = ApplicationServiceTestEnvironment.Actor();
		var item = ApplicationServiceTestEnvironment.Item();
		var source = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(actor.CharacterId),
			new[] { new InventoryPlacement(item.Id, 0, 0) });
		var target = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(CharacterId.New()));
		await SeedAsync(environment, item, source, target);
		GrantTransfer(environment, actor, source.Id, target.Id);

		var result = await environment.CreateInventoryMutationService().MoveCommittedAsync(
			actor, source.Id, target.Id, item.Id, 2, 1);

		Assert.IsTrue(result.Succeeded, result.Error?.Message);
		CollectionAssert.AreEquivalent(
			new[] { DomainKeys.Inventory( source.Id ), DomainKeys.Inventory( target.Id ) },
			result.Value!.Documents.Select( value => value.Address.Key ).ToArray() );
		Assert.IsNull(Find(environment, source.Id).Find(item.Id));
		Assert.AreEqual(
			new InventoryPlacement(item.Id, 2, 1),
			Find(environment, target.Id).Find(item.Id));
		Assert.AreEqual(0, environment.Provider.AllCallCount(DomainCollections.Inventories),
			"A character-to-character move must not scan the inventory store.");
	}

	[TestMethod]
	public async Task ForgedSourceIsRejectedWithoutMovingTheGloballyKnownItem()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var actor = ApplicationServiceTestEnvironment.Actor();
		var item = ApplicationServiceTestEnvironment.Item();
		var actualSource = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(actor.CharacterId),
			new[] { new InventoryPlacement(item.Id, 0, 0) });
		var forgedSource = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(actor.CharacterId));
		var target = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(CharacterId.New()));
		await environment.SeedAsync(unitOfWork =>
		{
			unitOfWork.Create(environment.Repositories.Items, DomainKeys.Item(item.Id), item);
			foreach (var inventory in new[] { actualSource, forgedSource, target })
				unitOfWork.Create(environment.Repositories.Inventories, DomainKeys.Inventory(inventory.Id), inventory);
		});
		GrantTransfer(environment, actor, forgedSource.Id, target.Id);

		var result = await environment.CreateInventoryMutationService().MoveAsync(
			actor, forgedSource.Id, target.Id, item.Id, 1, 1);

		Assert.AreEqual(ErrorCode.NotFound, result.Error!.Code);
		Assert.IsNotNull(Find(environment, actualSource.Id).Find(item.Id));
		Assert.IsEmpty(Find(environment, target.Id).Placements);
	}

	[TestMethod]
	public async Task MissingTransferCapabilityIsRejectedAndLeavesBothSidesUnchanged()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var actor = ApplicationServiceTestEnvironment.Actor();
		var item = ApplicationServiceTestEnvironment.Item();
		var source = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(actor.CharacterId),
			new[] { new InventoryPlacement(item.Id, 0, 0) });
		var target = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(CharacterId.New()));
		await SeedAsync(environment, item, source, target);
		environment.Grant(actor, source.Id, InventoryCapability.Move);
		environment.Grant(actor, target.Id, InventoryCapability.TransferIn);

		var result = await environment.CreateInventoryMutationService().MoveAsync(
			actor, source.Id, target.Id, item.Id, 1, 1);

		Assert.AreEqual(ErrorCode.Unauthorized, result.Error!.Code);
		Assert.IsNotNull(Find(environment, source.Id).Find(item.Id));
		Assert.IsEmpty(Find(environment, target.Id).Placements);
	}

	[TestMethod]
	public async Task LegitimateConnectionGrantCannotBeReusedByAForgedCharacter()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var legitimateActor = ApplicationServiceTestEnvironment.Actor();
		var forgedActor = legitimateActor with { CharacterId = CharacterId.New() };
		var item = ApplicationServiceTestEnvironment.Item();
		var source = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(legitimateActor.CharacterId),
			new[] { new InventoryPlacement(item.Id, 0, 0) });
		var target = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(CharacterId.New()));
		await SeedAsync(environment, item, source, target);
		GrantTransfer(environment, legitimateActor, source.Id, target.Id);

		var result = await environment.CreateInventoryMutationService().MoveAsync(
			forgedActor, source.Id, target.Id, item.Id, 1, 1);

		Assert.AreEqual(ErrorCode.Unauthorized, result.Error!.Code);
		Assert.IsNotNull(Find(environment, source.Id).Find(item.Id));
		Assert.IsEmpty(Find(environment, target.Id).Placements);
	}

	[TestMethod]
	public async Task BagsCannotMoveIntoThemselvesOrDescendants()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var actor = ApplicationServiceTestEnvironment.Actor();
		var selfBag = ApplicationServiceTestEnvironment.Item(ApplicationServiceTestEnvironment.BagDefinitionId);
		var selfSource = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(actor.CharacterId),
			new[] { new InventoryPlacement(selfBag.Id, 0, 0) });
		var selfTarget = ApplicationServiceTestEnvironment.Inventory(InventoryOwner.ParentItem(selfBag.Id));

		var outerBag = ApplicationServiceTestEnvironment.Item(ApplicationServiceTestEnvironment.BagDefinitionId);
		var innerBag = ApplicationServiceTestEnvironment.Item(ApplicationServiceTestEnvironment.BagDefinitionId);
		var descendantSource = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(actor.CharacterId),
			new[] { new InventoryPlacement(outerBag.Id, 0, 0) });
		var outerInventory = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.ParentItem(outerBag.Id),
			new[] { new InventoryPlacement(innerBag.Id, 0, 0) });
		var descendantTarget = ApplicationServiceTestEnvironment.Inventory(InventoryOwner.ParentItem(innerBag.Id));

		await environment.SeedAsync(unitOfWork =>
		{
			foreach (var item in new[] { selfBag, outerBag, innerBag })
				unitOfWork.Create(environment.Repositories.Items, DomainKeys.Item(item.Id), item);
			foreach (var inventory in new[]
				{ selfSource, selfTarget, descendantSource, outerInventory, descendantTarget })
				unitOfWork.Create(environment.Repositories.Inventories, DomainKeys.Inventory(inventory.Id), inventory);
			foreach (var bagInventory in new[] { selfTarget, outerInventory, descendantTarget })
			{
				var index = ApplicationServiceTestEnvironment.OwnerIndex(bagInventory, InventoryRoles.Bag);
				unitOfWork.Create(
					environment.Repositories.OwnerInventories,
					DomainKeys.OwnerInventory(index.Owner, index.Role),
					index);
			}
		});
		GrantTransfer(environment, actor, selfSource.Id, selfTarget.Id);
		GrantTransfer(environment, actor, descendantSource.Id, descendantTarget.Id);
		var service = environment.CreateInventoryMutationService();

		var selfResult = await service.MoveAsync(
			actor, selfSource.Id, selfTarget.Id, selfBag.Id, 0, 0);
		var descendantResult = await service.MoveAsync(
			actor, descendantSource.Id, descendantTarget.Id, outerBag.Id, 0, 0);

		Assert.AreEqual(ErrorCode.InvalidArgument, selfResult.Error!.Code);
		Assert.AreEqual(ErrorCode.InvalidArgument, descendantResult.Error!.Code);
		Assert.IsNotNull(Find(environment, selfSource.Id).Find(selfBag.Id));
		Assert.IsNotNull(Find(environment, descendantSource.Id).Find(outerBag.Id));
		Assert.IsEmpty(Find(environment, selfTarget.Id).Placements);
		Assert.IsEmpty(Find(environment, descendantTarget.Id).Placements);
		Assert.AreEqual(0, environment.Provider.AllCallCount(DomainCollections.Inventories),
			"Bag-cycle detection must use keyed owner-index probes, never a store scan.");
	}

	[TestMethod]
	public async Task InjectedCommitFailureLeavesBothInventoriesUnchanged()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var actor = ApplicationServiceTestEnvironment.Actor();
		var item = ApplicationServiceTestEnvironment.Item();
		var source = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(actor.CharacterId),
			new[] { new InventoryPlacement(item.Id, 0, 0) });
		var target = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(CharacterId.New()));
		await SeedAsync(environment, item, source, target);
		GrantTransfer(environment, actor, source.Id, target.Id);
		environment.Provider.FailNextCommit();

		var result = await environment.CreateInventoryMutationService().MoveAsync(
			actor, source.Id, target.Id, item.Id, 1, 1);

		Assert.AreEqual(ErrorCode.InternalError, result.Error!.Code);
		Assert.IsNotNull(Find(environment, source.Id).Find(item.Id));
		Assert.IsEmpty(Find(environment, target.Id).Placements);
	}

	[TestMethod]
	public async Task ConcurrentSourceOrDestinationCapabilityRevocationConflictsBeforeMoveCommit()
	{
		await AssertRevokedTransferConflictsAsync(revokeSource: true);
		await AssertRevokedTransferConflictsAsync(revokeSource: false);
	}

	private static async Task AssertRevokedTransferConflictsAsync(bool revokeSource)
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var actor = ApplicationServiceTestEnvironment.Actor();
		var item = ApplicationServiceTestEnvironment.Item();
		var source = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(actor.CharacterId),
			new[] { new InventoryPlacement(item.Id, 0, 0) });
		var target = ApplicationServiceTestEnvironment.Inventory(
			InventoryOwner.Character(CharacterId.New()));
		await SeedAsync(environment, item, source, target);
		var sourceSession = GrantSession(
			environment, actor, source.Id,
			InventoryCapability.Move | InventoryCapability.TransferOut);
		var targetSession = GrantSession(
			environment, actor, target.Id, InventoryCapability.TransferIn);
		environment.Provider.BeforeNextCommit(() =>
			environment.Access.RevokeSession(revokeSource ? sourceSession : targetSession));

		var result = await environment.CreateInventoryMutationService().MoveCommittedAsync(
			actor, source.Id, target.Id, item.Id, 1, 1);

		Assert.AreEqual(ErrorCode.Conflict, result.Error!.Code);
		Assert.IsNotNull(Find(environment, source.Id).Find(item.Id));
		Assert.IsEmpty(Find(environment, target.Id).Placements);
	}

	private static async Task SeedAsync(
		ApplicationServiceTestEnvironment environment,
		ItemRecord item,
		InventoryRecord source,
		InventoryRecord target)
	{
		await environment.SeedAsync(unitOfWork =>
		{
			unitOfWork.Create(environment.Repositories.Items, DomainKeys.Item(item.Id), item);
			unitOfWork.Create(environment.Repositories.Inventories, DomainKeys.Inventory(source.Id), source);
			unitOfWork.Create(environment.Repositories.Inventories, DomainKeys.Inventory(target.Id), target);
		});
	}

	private static void GrantTransfer(
		ApplicationServiceTestEnvironment environment,
		InventoryActor actor,
		InventoryId source,
		InventoryId target)
	{
		environment.Grant(actor, source, InventoryCapability.Move | InventoryCapability.TransferOut);
		environment.Grant(actor, target, InventoryCapability.TransferIn);
	}

	private static InteractionSessionId GrantSession(
		ApplicationServiceTestEnvironment environment,
		InventoryActor actor,
		InventoryId inventoryId,
		InventoryCapability capabilities)
	{
		var sessionId = InteractionSessionId.New();
		environment.OpenConnection(actor.ConnectionId);
		environment.Access.Grant(new InventoryGrant
		{
			ConnectionId = actor.ConnectionId,
			CharacterId = actor.CharacterId,
			InventoryId = inventoryId,
			Capabilities = capabilities,
			Kind = InventoryGrantKind.InteractionSession,
			SessionId = sessionId
		});
		return sessionId;
	}

	private static InventoryRecord Find(
		ApplicationServiceTestEnvironment environment,
		InventoryId id) => environment.Repositories.Inventories.Find(DomainKeys.Inventory(id))!.Value;
}
#nullable enable

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;

namespace Hexagon.V2.Tests.Architecture;

/// <summary>
/// Inclusion-completeness guard for the engine-facing layers: every file under
/// Code/V2/Runtime and Code/V2/Infrastructure is either compiled into this test project
/// or sits on the reviewed exclusion manifest with a reason — never silently untested.
/// </summary>
[TestClass]
public sealed class RuntimeInclusionTests
{
	[TestMethod]
	public void EveryEngineFacingFileIsCompiledOrOnTheReviewedExclusionManifest()
	{
		var compiled = CompiledEngineFacingFiles();
		var manifest = ReadExclusionManifest();
		var disk = EngineFacingFilesOnDisk();

		var missing = disk
			.Where( file => !compiled.Contains( file ) && !manifest.ContainsKey( file ) )
			.OrderBy( file => file, StringComparer.Ordinal )
			.ToArray();
		Assert.IsEmpty( missing,
			"Engine-facing files must be compiled into the test project or reviewed onto " +
			$"tests/CompileExclusions.txt with a reason: {string.Join( ", ", missing )}" );

		var overlapping = manifest.Keys
			.Where( compiled.Contains )
			.OrderBy( file => file, StringComparer.Ordinal )
			.ToArray();
		Assert.IsEmpty( overlapping,
			$"Manifest entries are stale; these files are compiled now: {string.Join( ", ", overlapping )}" );

		var vanished = manifest.Keys
			.Where( file => !disk.Contains( file ) )
			.OrderBy( file => file, StringComparer.Ordinal )
			.ToArray();
		Assert.IsEmpty( vanished,
			$"Manifest entries name files that no longer exist: {string.Join( ", ", vanished )}" );

		foreach ( var entry in manifest )
			Assert.IsFalse( string.IsNullOrWhiteSpace( entry.Value ),
				$"Manifest entry '{entry.Key}' must carry a non-empty reason." );
	}

	private static HashSet<string> CompiledEngineFacingFiles()
	{
		var project = XDocument.Load( TestProjectPath() );
		return project.Descendants( "Compile" )
			.Select( item => item.Attribute( "Include" )?.Value )
			.Where( include => include is not null && include.StartsWith( "../Code/V2/", StringComparison.Ordinal ) )
			.Select( include => include!["../Code/V2/".Length..].Replace( '\\', '/' ) )
			.Where( IsEngineFacing )
			.ToHashSet( StringComparer.Ordinal );
	}

	private static SortedDictionary<string, string> ReadExclusionManifest()
	{
		var manifest = new SortedDictionary<string, string>( StringComparer.Ordinal );
		foreach ( var line in File.ReadAllLines( ManifestPath() ) )
		{
			var trimmed = line.Trim();
			if ( trimmed.Length == 0 || trimmed.StartsWith( "#", StringComparison.Ordinal ) ) continue;
			var separator = trimmed.IndexOf( '|', StringComparison.Ordinal );
			Assert.IsGreaterThan( 0, separator, $"Manifest line must be '<path> | <reason>': {trimmed}" );
			var path = trimmed[..separator].Trim().Replace( '\\', '/' );
			var reason = trimmed[(separator + 1)..].Trim();
			Assert.IsTrue( IsEngineFacing( path ),
				$"Manifest may list only Runtime/Infrastructure files: {path}" );
			Assert.IsTrue( manifest.TryAdd( path, reason ), $"Manifest lists '{path}' twice." );
		}
		Assert.IsNotEmpty( manifest, "The exclusion manifest parsed to zero entries." );
		return manifest;
	}

	private static HashSet<string> EngineFacingFilesOnDisk()
	{
		var root = V2Root();
		return new[] { "Runtime", "Infrastructure" }
			.Select( layer => Path.Combine( root, layer ) )
			.Where( Directory.Exists )
			.SelectMany( layer => Directory.EnumerateFiles( layer, "*.cs", SearchOption.AllDirectories ) )
			.Select( file => Path.GetRelativePath( root, file ).Replace( '\\', '/' ) )
			.ToHashSet( StringComparer.Ordinal );
	}

	private static bool IsEngineFacing( string relativePath ) =>
		relativePath.StartsWith( "Runtime/", StringComparison.Ordinal ) ||
		relativePath.StartsWith( "Infrastructure/", StringComparison.Ordinal );

	private static string TestProjectPath() =>
		Path.Combine( TestsRoot(), "Hexagon.V2.Tests.csproj" );

	private static string ManifestPath() =>
		Path.Combine( TestsRoot(), "CompileExclusions.txt" );

	private static string V2Root() => Path.Combine( ProductRoot(), "Code", "V2" );
	private static string TestsRoot() => Path.Combine( ProductRoot(), "tests" );

	private static string ProductRoot()
	{
		var current = new DirectoryInfo( AppContext.BaseDirectory );
		while ( current is not null )
		{
			if ( Directory.Exists( Path.Combine( current.FullName, "Code", "V2" ) ) )
				return current.FullName;
			current = current.Parent;
		}

		throw new DirectoryNotFoundException( "Could not locate the Hexagon product root from the test output directory." );
	}
}
using Hexagon.V2.Kernel;
using Hexagon.V2.Kernel.Events;
using Hexagon.V2.Kernel.Policies;
using Hexagon.V2.Kernel.Schema;

namespace Hexagon.V2.Tests.Kernel;

[TestClass]
public sealed class PolicyAndEventTests
{
	[TestMethod]
	public void PolicyPipelineRequiresEveryHandlerToAllowInDeterministicOrder()
	{
		var calls = new List<string>();
		var pipeline = new PolicyPipeline<string>(
			new PolicyHandler<string>("built_in", new DelegatePolicy<string>(_ =>
			{
				calls.Add("built_in");
				return PolicyDecision.Allow();
			})),
			new[]
			{
				new PolicyHandler<string>("last", new DelegatePolicy<string>(_ =>
				{
					calls.Add("last");
					return PolicyDecision.Allow();
				}), 20),
				new PolicyHandler<string>("deny", new DelegatePolicy<string>(_ =>
				{
					calls.Add("deny");
					return PolicyDecision.Deny("No access.", ErrorCode.Unauthorized);
				}), 10)
			});

		var result = pipeline.Evaluate("context");

		Assert.IsTrue(result.Failed);
		Assert.AreEqual(ErrorCode.Unauthorized, result.Error!.Code);
		CollectionAssert.AreEqual(new[] { "built_in", "deny" }, calls);
	}

	[TestMethod]
	public void PolicyExceptionFailsClosedAndProducesDiagnostic()
	{
		PolicyDiagnostic? diagnostic = null;
		var pipeline = new PolicyPipeline<string>(
			new PolicyHandler<string>("built_in",
				new DelegatePolicy<string>(_ => throw new InvalidOperationException("boom"))),
			diagnostics: value => diagnostic = value);

		var result = pipeline.Evaluate("context");

		Assert.IsTrue(result.Failed);
		Assert.AreEqual(ErrorCode.PolicyFailed, result.Error!.Code);
		Assert.AreEqual("built_in", diagnostic!.HandlerId);
	}

	[TestMethod]
	public void SchemaRejectsCustomPolicyWithoutExactlyOneBuiltInPolicy()
	{
		var schema = new DelegateSchema(builder =>
			builder.RegisterPolicy("custom", new DelegatePolicy<string>(_ => PolicyDecision.Allow())));

		var report = SchemaCompiler.Validate(schema);

		Assert.IsTrue(report.Issues.Any(x => x.Message.Contains("Exactly one built-in", StringComparison.Ordinal)));
	}

	[TestMethod]
	public void PostCommitEventBusIsolatesFailureAndInvokesLaterHandlers()
	{
		var calls = new List<string>();
		var bus = new PostCommitEventBus<string>(new[]
		{
			new EventHandlerRegistration<string>("broken", new DelegateEventHandler<string>(_ =>
			{
				calls.Add("broken");
				throw new InvalidOperationException("boom");
			}), 0),
			new EventHandlerRegistration<string>("healthy", new DelegateEventHandler<string>(_ =>
				calls.Add("healthy")), 1)
		});

		var report = bus.Publish("committed");

		Assert.AreEqual(2, report.InvokedCount);
		Assert.HasCount(1, report.Failures);
		CollectionAssert.AreEqual(new[] { "broken", "healthy" }, calls);
	}

	[TestMethod]
	public void CompiledSchemaBuildsRegisteredPolicyAndEventPipelines()
	{
		var observed = string.Empty;
		var schema = new DelegateSchema(builder =>
		{
			builder.RegisterBuiltInPolicy("core", new DelegatePolicy<string>(_ => PolicyDecision.Allow()));
			builder.RegisterEventHandler("observer", new DelegateEventHandler<string>(value => observed = value));
		});
		var compiled = SchemaCompiler.Compile(schema).Value;

		var policy = compiled.CreatePolicyPipeline<string>();
		var eventReport = compiled.CreateEventBus<string>().Publish("done");

		Assert.IsTrue(policy.Succeeded);
		Assert.IsTrue(policy.Value.Evaluate("context").Succeeded);
		Assert.IsTrue(eventReport.Succeeded);
		Assert.AreEqual("done", observed);
	}

	[TestMethod]
	public void CompiledSchemaComposesRuntimePoliciesAfterMandatoryBuiltInInDeterministicOrder()
	{
		var calls = new List<string>();
		var schema = new DelegateSchema(builder =>
		{
			builder.RegisterBuiltInPolicy("core", new DelegatePolicy<string>(_ =>
			{
				calls.Add("core");
				return PolicyDecision.Allow();
			}), order: 1_000);
			builder.RegisterPolicy("schema.guard", new DelegatePolicy<string>(_ =>
			{
				calls.Add("schema.guard");
				return PolicyDecision.Allow();
			}), order: 10);
		});
		var compiled = SchemaCompiler.Compile(schema).Value;

		var pipeline = compiled.CreatePolicyPipeline<string>(new[]
		{
			new PolicyHandler<string>("runtime.later", new DelegatePolicy<string>(_ =>
			{
				calls.Add("runtime.later");
				return PolicyDecision.Allow();
			}), 30),
			new PolicyHandler<string>("runtime.authorization", new DelegatePolicy<string>(_ =>
			{
				calls.Add("runtime.authorization");
				return PolicyDecision.Deny("Runtime authorization denied.", ErrorCode.Unauthorized);
			}), 20)
		});

		Assert.IsTrue(pipeline.Succeeded, pipeline.Error?.Message);
		var result = pipeline.Value.Evaluate("context");

		Assert.IsTrue(result.Failed);
		Assert.AreEqual(ErrorCode.Unauthorized, result.Error!.Code);
		CollectionAssert.AreEqual(
			new[] { "core", "schema.guard", "runtime.authorization" },
			calls);
	}

	[TestMethod]
	public void CompiledSchemaRejectsInvalidOrDuplicateRuntimePolicyIdentifiers()
	{
		var schema = new DelegateSchema(builder =>
		{
			builder.RegisterBuiltInPolicy("core", new DelegatePolicy<string>(_ => PolicyDecision.Allow()));
			builder.RegisterPolicy("schema.guard", new DelegatePolicy<string>(_ => PolicyDecision.Allow()));
		});
		var compiled = SchemaCompiler.Compile(schema).Value;
		var allow = new DelegatePolicy<string>(_ => PolicyDecision.Allow());

		var invalid = compiled.CreatePolicyPipeline<string>(new[]
		{
			new PolicyHandler<string>("Runtime Bad", allow)
		});
		var schemaCollision = compiled.CreatePolicyPipeline<string>(new[]
		{
			new PolicyHandler<string>("schema.guard", allow)
		});
		var runtimeCollision = compiled.CreatePolicyPipeline<string>(new[]
		{
			new PolicyHandler<string>("runtime.guard", allow),
			new PolicyHandler<string>("runtime.guard", allow)
		});

		Assert.AreEqual(ErrorCode.InvalidIdentifier, invalid.Error!.Code);
		Assert.AreEqual(ErrorCode.DuplicateRegistration, schemaCollision.Error!.Code);
		Assert.AreEqual(ErrorCode.DuplicateRegistration, runtimeCollision.Error!.Code);
	}

	private sealed class DelegatePolicy<T> : IPolicy<T>
	{
		private readonly Func<T, PolicyDecision> _evaluate;
		public DelegatePolicy(Func<T, PolicyDecision> evaluate) => _evaluate = evaluate;
		public PolicyDecision Evaluate(T context) => _evaluate(context);
	}

	private sealed class DelegateEventHandler<T> : IEventHandler<T>
	{
		private readonly Action<T> _handle;
		public DelegateEventHandler(Action<T> handle) => _handle = handle;
		public void Handle(T @event) => _handle(@event);
	}

	private sealed class DelegateSchema : IHexSchema
	{
		private readonly Action<SchemaBuilder> _configure;
		public DelegateSchema(Action<SchemaBuilder> configure) => _configure = configure;
		public string Id => "test_schema";
		public void Configure(SchemaBuilder builder) => _configure(builder);
	}
}
#nullable enable

using Hexagon.V2.Networking;

namespace Hexagon.V2.Tests.Networking;

[TestClass]
public sealed class ApplicationConnectionLatchTests
{
	[TestMethod]
	public void EveryConditionOrderProducesExactlyOneNotificationAfterHello()
	{
		var conditions = new Func<ApplicationConnectionLatch, bool>[]
		{
			static latch => latch.ObserveNonceBound(),
			static latch => latch.ObserveHelloIssued(),
			static latch => latch.ObserveHostReady()
		};
		var permutations = new[]
		{
			new[] { 0, 1, 2 }, new[] { 0, 2, 1 }, new[] { 1, 0, 2 },
			new[] { 1, 2, 0 }, new[] { 2, 0, 1 }, new[] { 2, 1, 0 }
		};

		foreach ( var order in permutations )
		{
			var latch = new ApplicationConnectionLatch();
			var results = order.Select( index => conditions[index]( latch ) ).ToArray();
			CollectionAssert.AreEqual( new[] { false, false, true }, results );
			Assert.AreEqual( ApplicationConnectionState.Notifying, latch.State );
			Assert.IsFalse( latch.IsConnected );
			Assert.IsTrue( latch.CompleteNotification( true ) );
			Assert.IsTrue( latch.IsConnected );
			Assert.IsFalse( latch.ObserveHostReady() );
			Assert.IsFalse( latch.ObserveNonceBound() );
			Assert.IsFalse( latch.ObserveHelloIssued() );
		}
	}

	[TestMethod]
	public void CommandsRemainBlockedUntilNotificationSucceeds()
	{
		var latch = new ApplicationConnectionLatch();
		Assert.IsFalse( latch.ObserveNonceBound() );
		Assert.IsFalse( latch.ObserveHostReady() );
		Assert.IsFalse( latch.IsConnected );
		Assert.IsTrue( latch.ObserveHelloIssued() );
		Assert.IsFalse( latch.IsConnected );
		Assert.IsTrue( latch.CompleteNotification( true ) );
		Assert.IsTrue( latch.IsConnected );
	}

	[TestMethod]
	public void FailedOrDisconnectedNotificationCannotRestoreAuthority()
	{
		var failed = new ApplicationConnectionLatch();
		Assert.IsFalse( failed.ObserveNonceBound() );
		Assert.IsFalse( failed.ObserveHostReady() );
		Assert.IsTrue( failed.ObserveHelloIssued() );
		Assert.IsFalse( failed.CompleteNotification( false ) );
		Assert.AreEqual( ApplicationConnectionState.Failed, failed.State );
		Assert.IsFalse( failed.IsConnected );
		Assert.IsFalse( failed.ObserveNonceBound() );

		var disconnected = new ApplicationConnectionLatch();
		Assert.IsFalse( disconnected.ObserveHostReady() );
		Assert.IsFalse( disconnected.ObserveNonceBound() );
		Assert.IsTrue( disconnected.ObserveHelloIssued() );
		Assert.IsFalse( disconnected.Disconnect() );
		Assert.IsFalse( disconnected.CompleteNotification( true ) );
		Assert.AreEqual( ApplicationConnectionState.Disconnected, disconnected.State );
		Assert.IsFalse( disconnected.IsConnected );
	}

	[TestMethod]
	public async Task ConcurrentConditionsCannotLoseOrDuplicateNotification()
	{
		for ( var iteration = 0; iteration < 1_000; iteration++ )
		{
			var latch = new ApplicationConnectionLatch();
			var results = await Task.WhenAll(
				Task.Run( latch.ObserveNonceBound ),
				Task.Run( latch.ObserveHostReady ),
				Task.Run( latch.ObserveHelloIssued ) );
			Assert.AreEqual( 1, results.Count( value => value ) );
			Assert.IsTrue( latch.CompleteNotification( true ) );
			Assert.IsTrue( latch.IsConnected );
		}
	}
}
#nullable enable

using Hexagon.V2.Networking;

namespace Hexagon.V2.Tests.Networking;

[TestClass]
public sealed class ClientSessionRetryScheduleTests
{
	[TestMethod]
	public void AttemptsImmediatelyThenBacksOffToTheTwoSecondCap()
	{
		var schedule = new ClientSessionRetrySchedule( 1_000 );

		Assert.IsTrue( schedule.TryBeginAttempt( 0 ) );
		Assert.IsFalse( schedule.TryBeginAttempt( 249 ) );
		Assert.IsTrue( schedule.TryBeginAttempt( 250 ) );
		Assert.IsFalse( schedule.TryBeginAttempt( 749 ) );
		Assert.IsTrue( schedule.TryBeginAttempt( 750 ) );
		Assert.IsFalse( schedule.TryBeginAttempt( 1_749 ) );
		Assert.IsTrue( schedule.TryBeginAttempt( 1_750 ) );
		Assert.IsFalse( schedule.TryBeginAttempt( 3_749 ) );
		Assert.IsTrue( schedule.TryBeginAttempt( 3_750 ) );
		Assert.IsFalse( schedule.TryBeginAttempt( 5_749 ) );
		Assert.IsTrue( schedule.TryBeginAttempt( 5_750 ) );
	}

	[TestMethod]
	public void ExactHelloOrDisposalStopsEveryLaterAttempt()
	{
		var hello = new ClientSessionRetrySchedule( 1_000 );
		Assert.IsTrue( hello.TryBeginAttempt( 0 ) );
		Assert.IsTrue( hello.Complete() );
		Assert.IsFalse( hello.Complete() );
		Assert.IsTrue( hello.IsComplete );
		Assert.IsFalse( hello.TryBeginAttempt( long.MaxValue ) );

		var disposed = new ClientSessionRetrySchedule( 1_000 );
		Assert.IsTrue( disposed.Complete() );
		Assert.IsFalse( disposed.TryBeginAttempt( 0 ) );
	}

	[TestMethod]
	public async Task ConcurrentPollingCannotBeginMoreThanOneAttemptPerWindow()
	{
		for ( var iteration = 0; iteration < 1_000; iteration++ )
		{
			var schedule = new ClientSessionRetrySchedule( 1_000 );
			var initial = await Task.WhenAll(
				Enumerable.Range( 0, 8 ).Select( _ => Task.Run( () => schedule.TryBeginAttempt( 0 ) ) ) );
			Assert.AreEqual( 1, initial.Count( result => result ) );

			var retry = await Task.WhenAll(
				Enumerable.Range( 0, 8 ).Select( _ => Task.Run( () => schedule.TryBeginAttempt( 250 ) ) ) );
			Assert.AreEqual( 1, retry.Count( result => result ) );
		}
	}
}
#nullable enable

using System;
using Hexagon.V2.Runtime;

namespace Hexagon.V2.Tests.Runtime;

[TestClass]
public sealed class SpawnSlotAllocatorTests
{
	[TestMethod]
	public void SequentialConnectionsReceiveDistinctLowestSlots()
	{
		var allocator = new SpawnSlotAllocator();

		Assert.AreEqual( 0, allocator.Acquire( Guid.NewGuid() ) );
		Assert.AreEqual( 1, allocator.Acquire( Guid.NewGuid() ) );
		Assert.AreEqual( 2, allocator.Acquire( Guid.NewGuid() ) );
	}

	[TestMethod]
	public void RepeatedAcquireReturnsTheExistingLease()
	{
		var allocator = new SpawnSlotAllocator();
		var connection = Guid.NewGuid();

		Assert.AreEqual( 0, allocator.Acquire( connection ) );
		Assert.AreEqual( 0, allocator.Acquire( connection ) );
		Assert.AreEqual( 1, allocator.Acquire( Guid.NewGuid() ) );
	}

	[TestMethod]
	public void ReleasedSlotIsReusedBeforeHigherSlots()
	{
		var allocator = new SpawnSlotAllocator();
		var first = Guid.NewGuid();
		var second = Guid.NewGuid();
		var third = Guid.NewGuid();
		Assert.AreEqual( 0, allocator.Acquire( first ) );
		Assert.AreEqual( 1, allocator.Acquire( second ) );
		Assert.AreEqual( 2, allocator.Acquire( third ) );

		Assert.IsTrue( allocator.Release( second ) );
		Assert.AreEqual( 1, allocator.Acquire( Guid.NewGuid() ) );
	}

	[TestMethod]
	public void ConfiguredSpawnPointsAreUsedExactlyBeforeOverflow()
	{
		Assert.AreEqual( new SpawnSlotPlacement( 0, 0, 0 ), SpawnSlotAllocator.Describe( 0, 3 ) );
		Assert.AreEqual( new SpawnSlotPlacement( 1, 0, 0 ), SpawnSlotAllocator.Describe( 1, 3 ) );
		Assert.AreEqual( new SpawnSlotPlacement( 2, 0, 0 ), SpawnSlotAllocator.Describe( 2, 3 ) );
		Assert.AreEqual( new SpawnSlotPlacement( 0, 1, 0 ), SpawnSlotAllocator.Describe( 3, 3 ) );
		Assert.AreEqual( new SpawnSlotPlacement( 1, 1, 0 ), SpawnSlotAllocator.Describe( 4, 3 ) );
	}

	[TestMethod]
	public void OverflowUsesDeterministicEightPositionRings()
	{
		Assert.AreEqual( new SpawnSlotPlacement( 0, 1, 0 ), SpawnSlotAllocator.Describe( 1, 1 ) );
		Assert.AreEqual( new SpawnSlotPlacement( 0, 1, 7 ), SpawnSlotAllocator.Describe( 8, 1 ) );
		Assert.AreEqual( new SpawnSlotPlacement( 0, 2, 0 ), SpawnSlotAllocator.Describe( 9, 1 ) );
		Assert.AreEqual( 64.0, SpawnSlotAllocator.Describe( 1, 1 ).Radius );
		Assert.AreEqual( Math.PI / 4.0, SpawnSlotAllocator.Describe( 2, 1 ).AngleRadians, 1e-12 );
		Assert.AreEqual( 128.0, SpawnSlotAllocator.Describe( 9, 1 ).Radius );
	}
}