using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NovelSoftware.Application.Agent;
using NovelSoftware.Application.Dtos;
using NovelSoftware.Application.Services;
namespace NovelSoftware.Tests;
///
/// Covers the list endpoints, which sort and aggregate in SQL rather than in memory.
/// SQLite is fussier than the in-memory provider about what it will translate — ordering
/// by a DateTimeOffset, for one — so these have to run against real SQLite to be worth anything.
///
public class ListingTests : IDisposable
{
private readonly TestDatabase _db = new();
private readonly ProjectService _projects;
private readonly ChapterService _chapters;
private readonly SceneService _scenes;
private readonly CharacterService _characters;
public ListingTests()
{
_projects = new ProjectService(_db.Context);
_chapters = new ChapterService(_db.Context);
_scenes = new SceneService(_db.Context);
_characters = new CharacterService(_db.Context);
}
[Fact]
public async Task Projects_are_listed_most_recently_updated_first()
{
var older = await _projects.CreateAsync(new CreateProjectRequest("Older Book"));
var newer = await _projects.CreateAsync(new CreateProjectRequest("Newer Book"));
// Touching the older project should float it to the top.
await _projects.UpdateAsync(older.Id, new UpdateProjectRequest(Logline: "Revised."));
var listed = await _projects.ListAsync();
listed.Select(p => p.Title).Should().Equal("Older Book", "Newer Book");
listed.Select(p => p.Id).Should().Contain(newer.Id);
}
[Fact]
public async Task Project_summaries_aggregate_counts_and_words_across_chapters()
{
var project = await _projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
await _characters.CreateAsync(project.Id, new CreateCharacterRequest("Ines"));
await _characters.CreateAsync(project.Id, new CreateCharacterRequest("Mara"));
var first = await _chapters.CreateAsync(project.Id, new CreateChapterRequest("Landfall"));
var second = await _chapters.CreateAsync(project.Id, new CreateChapterRequest("The Harbour"));
await _scenes.CreateAsync(first.Id, new CreateSceneRequest("Dawn", Prose: "One two three"));
await _scenes.CreateAsync(second.Id, new CreateSceneRequest("Dusk", Prose: "Four five"));
var summary = (await _projects.ListAsync()).Single();
summary.CharacterCount.Should().Be(2);
summary.ChapterCount.Should().Be(2);
summary.WordCount.Should().Be(5);
}
[Fact]
public async Task A_project_with_no_chapters_reports_zero_words_rather_than_failing()
{
await _projects.CreateAsync(new CreateProjectRequest("Empty"));
var summary = (await _projects.ListAsync()).Single();
summary.WordCount.Should().Be(0);
summary.ChapterCount.Should().Be(0);
}
[Fact]
public async Task Chapters_are_listed_in_manuscript_order_with_scene_totals()
{
var project = await _projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
var second = await _chapters.CreateAsync(project.Id, new CreateChapterRequest("Second", Number: 2));
var first = await _chapters.CreateAsync(project.Id, new CreateChapterRequest("First", Number: 1));
await _scenes.CreateAsync(second.Id, new CreateSceneRequest("A", Prose: "One two"));
await _scenes.CreateAsync(second.Id, new CreateSceneRequest("B", Prose: "Three"));
var listed = await _chapters.ListAsync(project.Id);
listed.Select(c => c.Title).Should().Equal("First", "Second");
listed.Single(c => c.Id == second.Id).SceneCount.Should().Be(2);
listed.Single(c => c.Id == second.Id).WordCount.Should().Be(3);
listed.Single(c => c.Id == first.Id).WordCount.Should().Be(0);
}
[Fact]
public async Task Conversations_are_listed_most_recently_updated_first()
{
var project = await _projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
var agent = new NovelAgentService(
_db.Context,
new ScriptedModelClient([[new AgentTextBlock("Reply.")]]),
new NovelAgentToolset(_projects, _characters, new OutlineService(_db.Context), _chapters, _scenes),
Options.Create(new AgentOptions()),
NullLogger.Instance);
await agent.SendMessageAsync(project.Id, new SendAgentMessageRequest("First question."));
await agent.SendMessageAsync(project.Id, new SendAgentMessageRequest("Second question."));
var listed = await agent.ListConversationsAsync(project.Id);
listed.Should().HaveCount(2);
listed[0].Title.Should().Be("Second question.");
listed[0].MessageCount.Should().Be(2);
}
[Fact]
public async Task Characters_are_listed_by_role_then_name()
{
var project = await _projects.CreateAsync(new CreateProjectRequest("The Salt Road"));
await _characters.CreateAsync(project.Id, new CreateCharacterRequest(
"Zeno", NovelSoftware.Domain.CharacterRole.Supporting));
await _characters.CreateAsync(project.Id, new CreateCharacterRequest(
"Ines", NovelSoftware.Domain.CharacterRole.Protagonist));
await _characters.CreateAsync(project.Id, new CreateCharacterRequest(
"Anders", NovelSoftware.Domain.CharacterRole.Supporting));
var listed = await _characters.ListAsync(project.Id);
listed.Select(c => c.Name).Should().Equal("Ines", "Anders", "Zeno");
}
public void Dispose() => _db.Dispose();
}