79 lines
6.1 KiB
C#
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]);
|
|
}
|
|
}
|