using Xunit; using FluentAssertions; using NSubstitute; using System; using System.Text.Json; using FreeCode.Core.Models; using FreeCode.Core.Enums; namespace FreeCode.Tests.Unit.Models; public sealed class MessageTests { [Fact] public void Message_HoldsRequiredFields() { var timestamp = new DateTime(2026, 4, 6, 10, 0, 0, DateTimeKind.Utc); var message = new Message { MessageId = "msg-1", Role = MessageRole.User, Content = "hello", Timestamp = timestamp, ToolUseId = "tool-1", ToolName = "bash" }; message.MessageId.Should().Be("msg-1"); message.Role.Should().Be(MessageRole.User); message.Content.Should().Be("hello"); message.Timestamp.Should().Be(timestamp); message.ToolUseId.Should().Be("tool-1"); message.ToolName.Should().Be("bash"); } [Fact] public void Message_DefaultTimestampIsUtcNowBased() { var before = DateTime.UtcNow.AddSeconds(-1); var message = new Message { MessageId = "msg-1", Role = MessageRole.Assistant, Content = "reply" }; var after = DateTime.UtcNow.AddSeconds(1); message.Timestamp.Should().BeOnOrAfter(before); message.Timestamp.Should().BeOnOrBefore(after); } [Fact] public void Message_AllowsNullContent() { var message = new Message { MessageId = "msg-1", Role = MessageRole.User, Content = null, Timestamp = new DateTime(2026, 4, 6, 10, 0, 0, DateTimeKind.Utc) }; message.Content.Should().BeNull(); } [Fact] public void Message_CanStoreStringContent() { var message = CreateMessage(content: "plain text"); message.Content.Should().Be("plain text"); } [Fact] public void Message_CanStoreJsonContent() { var payload = ParseJson("{\"type\":\"text\",\"value\":\"hi\"}"); var message = CreateMessage(content: payload); message.Content.Should().BeOfType(); ((JsonElement)message.Content!).GetProperty("type").GetString().Should().Be("text"); } [Fact] public void Message_CanStoreSubstitutedContentObject() { var disposable = new object(); var message = CreateMessage(content: disposable); message.Content.Should().BeSameAs(disposable); } [Fact] public void Message_ToolFieldsDefaultToNull() { var message = CreateMessage(); message.ToolUseId.Should().BeNull(); message.ToolName.Should().BeNull(); } [Fact] public void Message_CanStoreToolMetadata() { var message = CreateMessage(toolUseId: "tool-1", toolName: "read"); message.ToolUseId.Should().Be("tool-1"); message.ToolName.Should().Be("read"); } [Fact] public void Message_UsesValueEqualityForEquivalentValues() { var timestamp = new DateTime(2026, 4, 6, 10, 0, 0, DateTimeKind.Utc); var left = CreateMessage(timestamp: timestamp); var right = CreateMessage(timestamp: timestamp); left.Should().Be(right); } [Fact] public void Message_IsNotEqualWhenRoleDiffers() { var timestamp = new DateTime(2026, 4, 6, 10, 0, 0, DateTimeKind.Utc); var left = CreateMessage(role: MessageRole.User, timestamp: timestamp); var right = CreateMessage(role: MessageRole.Assistant, timestamp: timestamp); left.Should().NotBe(right); } [Fact] public void Message_WithExpressionClonesRecordWithChange() { var original = CreateMessage(content: "before"); var clone = original with { Content = "after" }; clone.MessageId.Should().Be(original.MessageId); clone.Role.Should().Be(original.Role); clone.Content.Should().Be("after"); original.Content.Should().Be("before"); } [Fact] public void Message_AllowsEmptyMessageId() { var message = CreateMessage(messageId: string.Empty); message.MessageId.Should().BeEmpty(); } [Fact] public void Message_AllowsEmptyToolUseId() { var message = CreateMessage(toolUseId: string.Empty); message.ToolUseId.Should().BeEmpty(); } [Fact] public void Message_AllowsEmptyToolName() { var message = CreateMessage(toolName: string.Empty); message.ToolName.Should().BeEmpty(); } [Theory] [InlineData(MessageRole.User)] [InlineData(MessageRole.Assistant)] [InlineData(MessageRole.System)] [InlineData(MessageRole.Tool)] public void Message_HoldsEachRole(MessageRole role) { var message = CreateMessage(role: role); message.Role.Should().Be(role); } private static Message CreateMessage( string messageId = "msg-1", MessageRole role = MessageRole.User, object? content = null, DateTime? timestamp = null, string? toolUseId = null, string? toolName = null) { return new Message { MessageId = messageId, Role = role, Content = content ?? "hello", Timestamp = timestamp ?? new DateTime(2026, 4, 6, 10, 0, 0, DateTimeKind.Utc), ToolUseId = toolUseId, ToolName = toolName }; } private static JsonElement ParseJson(string json) { using var document = JsonDocument.Parse(json); return document.RootElement.Clone(); } }