PlotDirector/PlotLine.Tests/CharacterProfileAgeTests.cs

79 lines
6.1 KiB
C#

using System.Reflection;
using PlotLine.Data;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
internal static class CharacterProfileAgeTests
{
private static void Check(bool value, string message) { if (!value) throw new Exception(message); }
public static void ProfileUsesStoryDate()
{
var character = new Character { CharacterID = 1, BirthDate = new DateTime(1965, 9, 1) };
var first = new Scene { SceneID = 900, TimeModeName = "Exact Date", StartDateTime = new DateTime(1980, 5, 7) };
var later = new Scene { SceneID = 2, TimeModeName = "Exact Date", StartDateTime = new DateTime(1982, 12, 24) };
SceneCharacter[] appearances = [new() { CharacterID = 1, SceneID = 2 }, new() { CharacterID = 1, SceneID = 900 }];
var result = CharacterAgeDisplayService.GetProfileAge(character, [first, later], appearances);
Check(result.DisplayText == "14 years, 8 months" && result.ReferenceScene?.SceneID == 900, "Age must use first narrative dated appearance, not current date or minimum scene ID.");
character.AgeReferenceSceneID = 2;
result = CharacterAgeDisplayService.GetProfileAge(character, [first, later], appearances);
Check(result.DisplayText == "17 years, 3 months" && result.Context == "At age reference scene", "Explicit age reference must take precedence.");
character.AgeReferenceSceneID = null;
first.StartDateTime = null;
Check(CharacterAgeDisplayService.GetProfileAge(character, [first, later], appearances).ReferenceScene?.SceneID == 2, "Undated appearance must not invent a reference date.");
later.StartDateTime = null;
result = CharacterAgeDisplayService.GetProfileAge(character, [first, later], appearances);
Check(result.ReferenceScene is null && result.Context.Contains("dated story scene"), "Known DOB without chronology must explain why age is unavailable.");
character.AgeAtSeriesStart = 17;
Check(CharacterAgeDisplayService.GetProfileAge(character, [], []).DisplayText == "About 17", "Approximate fallback must stay available.");
character.BirthDate = null; character.AgeAtSeriesStart = null;
Check(CharacterAgeDisplayService.GetProfileAge(character, [], []).DisplayText == "Unknown", "Missing date/age must stay unknown.");
}
public static void DetailAndSaveUseCanonicalBirthDate()
{
var character = new Character { CharacterID = 1, ProjectID = 1, CharacterName = "Nora", BirthDate = new DateTime(1965, 9, 1), AgeReferenceSceneID = 900 };
var timeline = new TimelineData { Project = new Project { ProjectID = 1 }, Books = [new Book { BookID = 1 }], Chapters = [new Chapter { ChapterID = 1, BookID = 1 }],
Scenes = [new Scene { SceneID = 900, ChapterID = 1, TimeModeName = "Exact Date", StartDateTime = new DateTime(1980, 5, 7) }] };
Character? saved = null;
var repository = Proxy<ICharacterRepository>((method, args) => method switch {
"GetCharacterAsync" => Task.FromResult<Character?>(character),
"ListCharactersAsync" => Task.FromResult<IReadOnlyList<Character>>([character]),
"GetTimelineAsync" => Task.FromResult(((IReadOnlyList<Character>)[character], (IReadOnlyList<SceneCharacter>)[new SceneCharacter { CharacterID = 1, SceneID = 900 }])),
"GetLookupsAsync" => Task.FromResult(new CharacterLookupData()),
"SaveCharacterAsync" => Capture((Character)args![0]!),
_ => Default<ICharacterRepository>(method) });
Task<int> Capture(Character value) { saved = value; return Task.FromResult(1); }
var service = new CharacterService(
Proxy<IProjectRepository>((_, _) => Task.FromResult<Project?>(timeline.Project)),
Proxy<IBookRepository>(), repository, Proxy<IProjectCollaborationRepository>(), Proxy<ISubscriptionService>(),
Proxy<IProjectActivityService>(), Proxy<ICurrentUserService>(), Proxy<IVisualIdentityImageService>(), Proxy<ICharacterEnrichmentService>(),
Proxy<ITimelineRepository>((_, _) => Task.FromResult(timeline)));
var detail = service.GetCharacterDetailAsync(1).GetAwaiter().GetResult()!;
Check(detail.DisplayAge == "14 years, 8 months" && detail.AgeReferenceScene?.SceneID == 900, "Detail runtime must supply canonical chronology to age service.");
Check(detail.Character.BirthDate == character.BirthDate, "Detail must keep authored DOB independently of generated analysis.");
service.SaveCharacterAsync(new CharacterEditViewModel { CharacterID = 1, ProjectID = 1, CharacterName = "Nora", BirthDate = new DateTime(1965, 9, 1) }).GetAwaiter().GetResult();
Check(saved?.BirthDate == new DateTime(1965, 9, 1) && saved.AgeReferenceSceneID == 900, "Date-only save must preserve date and existing reference scene.");
service.SaveCharacterAsync(new CharacterEditViewModel { CharacterID = 1, ProjectID = 1, CharacterName = "Nora", BirthDate = null }).GetAwaiter().GetResult();
Check(saved?.BirthDate is null, "Birth date must remain optional and clearable.");
}
private static T Proxy<T>(Func<string, object?[]?, object?>? handler = null) where T : class
{
var result = DispatchProxy.Create<T, StoryPointProxy>();
((StoryPointProxy)(object)result).Handler = handler ?? ((method, _) => Default<T>(method));
return result;
}
private static object? Default<T>(string name)
{
var type = typeof(T).GetMethods().First(x => x.Name == name).ReturnType;
if (type == typeof(Task)) return Task.CompletedTask;
if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(Task<>)) return type.IsValueType ? Activator.CreateInstance(type) : null;
var valueType = type.GenericTypeArguments[0];
object? value = valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)
? Array.CreateInstance(valueType.GenericTypeArguments[0], 0)
: valueType.IsValueType ? Activator.CreateInstance(valueType) : null;
return typeof(Task).GetMethod(nameof(Task.FromResult))!.MakeGenericMethod(valueType).Invoke(null, [value]);
}
}