internal static partial class Program
{
    static async Task TestEntryUnknownActionAsync()
    {
        var entry = NewEntry();
        var response = await entry.HandleCommandAsync("""{"action":"unknown.action"}""");
        using var doc = JsonDocument.Parse(response);

        Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false.");
        Assert(doc.RootElement.GetProperty("error").GetString()!.Contains("Unknown action"), "Expected unknown action error.");
    }

    static async Task TestEntryInvalidJsonAsync()
    {
        var entry = NewEntry();
        var response = await entry.HandleCommandAsync("{\"action\":\"fragments.list\"");
        using var doc = JsonDocument.Parse(response);

        Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false.");
        Assert(doc.RootElement.GetProperty("error").GetString()!.Contains("Invalid command JSON"), "Expected invalid JSON error.");
    }

    static async Task TestEntryGetMissingReturnsNullDataAsync()
    {
        var entry = NewEntry();
        var request = JsonSerializer.Serialize(new
        {
            action = "fragments.get",
            id = Guid.NewGuid().ToString(),
        });
        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);

        Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true.");
        Assert(doc.RootElement.GetProperty("data").ValueKind == JsonValueKind.Null, "Expected data=null for missing fragment.");
    }

    static async Task TestEntryCreateMissingPayloadAsync()
    {
        var entry = NewEntry();
        var response = await entry.HandleCommandAsync("""{"action":"fragments.create"}""");
        using var doc = JsonDocument.Parse(response);

        Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false.");
        Assert(doc.RootElement.GetProperty("error").GetString()!.Contains("payload", StringComparison.OrdinalIgnoreCase), "Expected payload validation error.");
    }

    static async Task TestEntryEntriesSaveMergeAsync()
    {
        var entry = NewEntry();
        var firstPath = await SaveEntryForTestAsync(entry, "2026-02-22", """
Date: 2026-02-22
## Summary
old summary text
""");

        var mergeRequest = JsonSerializer.Serialize(new
        {
            action = "entries.save",
            payload = new
            {
                filePath = firstPath,
                mode = "Daily",
                content = """
Date: 2026-02-22
## Summary
new summary text
## Reflection
new reflection text
"""
            }
        });

        var mergeResponse = await entry.HandleCommandAsync(mergeRequest);
        using var mergeDoc = JsonDocument.Parse(mergeResponse);
        Assert(mergeDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save daily merge.");

        var loadedAfterMerge = await LoadEntryForTestAsync(entry, firstPath);
        Assert(loadedAfterMerge.Contains("new summary text", StringComparison.Ordinal), "Expected merged entry to contain new summary text.");
        Assert(!loadedAfterMerge.Contains("old summary text", StringComparison.Ordinal), "Expected merged entry to replace old summary section.");
        Assert(loadedAfterMerge.Contains("new reflection text", StringComparison.Ordinal), "Expected merged entry to contain new reflection section.");

        var fragmentSaveRequest = JsonSerializer.Serialize(new
        {
            action = "entries.save",
            payload = new
            {
                filePath = firstPath,
                mode = "Fragment",
                content = "!NOTE\nfragment append text"
            }
        });

        var fragmentResponse = await entry.HandleCommandAsync(fragmentSaveRequest);
        using var fragmentDoc = JsonDocument.Parse(fragmentResponse);
        Assert(fragmentDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save fragment mode.");

        var loadedAfterFragment = await LoadEntryForTestAsync(entry, firstPath);
        Assert(loadedAfterFragment.Contains("fragment append text", StringComparison.Ordinal), "Expected fragment append text in saved entry.");
    }

    static async Task TestEntryEntriesLoadAsync()
    {
        var entry = NewEntry();
        var content = """
Date: 2026-02-22
## Summary
hello world
""";
        var filePath = await SaveEntryForTestAsync(entry, "2026-02-22", content);

        var request = JsonSerializer.Serialize(new
        {
            action = "entries.load",
            payload = new
            {
                filePath
            }
        });

        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);
        Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.load.");

        var data = doc.RootElement.GetProperty("data");
        var entryDto = data.GetProperty("Entry");
        Assert(entryDto.GetProperty("Date").GetString() == "2026-02-22", "Expected parsed date from entries.load.");
        Assert(entryDto.GetProperty("RawContent").GetString() == content, "Expected raw content from entries.load.");
        Assert(data.GetProperty("FileName").GetString() == "2026-02-22.md", "Expected file name from entries.load.");
    }

    static async Task TestEntryEntriesListAsync()
    {
        var entry = NewEntry();
        await SaveEntryForTestAsync(entry, "2026-02-03", "c");
        await SaveEntryForTestAsync(entry, "2026-02-01", "a");

        var request = JsonSerializer.Serialize(new
        {
            action = "entries.list",
            payload = new { }
        });

        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);
        Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list.");

        var data = doc.RootElement.GetProperty("data");
        Assert(data.ValueKind == JsonValueKind.Array, "Expected entries.list data array.");
        Assert(data.GetArrayLength() == 2, "Expected entries.list to return seeded markdown entries.");
        Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Expected entries.list sort order by file name.");
        Assert(data[1].GetProperty("FileName").GetString() == "2026-02-03.md", "Expected entries.list sort order by file name.");
    }

    static async Task TestEntryTemplatesCrudExcludesFromEntriesListAsync()
    {
        var entry = NewEntry();
        await SaveEntryForTestAsync(entry, "2026-02-03", "daily entry");

        var saveRequest = JsonSerializer.Serialize(new
        {
            action = "templates.save",
            payload = new
            {
                name = "Weekly Review",
                content = "# Weekly Review\n\n## Wins\n- one"
            }
        });
        var saveResponse = await entry.HandleCommandAsync(saveRequest);
        using var saveDoc = JsonDocument.Parse(saveResponse);
        Assert(saveDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.save.");

        var templatePath = saveDoc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? "";
        Assert(templatePath.EndsWith("Weekly%20Review.template.md", StringComparison.OrdinalIgnoreCase), "Template path should be canonical db://template path.");

        var listTemplatesRequest = JsonSerializer.Serialize(new
        {
            action = "templates.list",
            payload = new { }
        });
        var listTemplatesResponse = await entry.HandleCommandAsync(listTemplatesRequest);
        using var listTemplatesDoc = JsonDocument.Parse(listTemplatesResponse);
        Assert(listTemplatesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.list.");
        var templateItems = listTemplatesDoc.RootElement.GetProperty("data");
        Assert(templateItems.GetArrayLength() == 1, "Expected one template in templates.list.");
        Assert(templateItems[0].GetProperty("FileName").GetString() == "Weekly Review.template.md", "Template file name mismatch.");

        var listEntriesRequest = JsonSerializer.Serialize(new
        {
            action = "entries.list",
            payload = new { }
        });
        var listEntriesResponse = await entry.HandleCommandAsync(listEntriesRequest);
        using var listEntriesDoc = JsonDocument.Parse(listEntriesResponse);
        Assert(listEntriesDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.list.");
        var entryItems = listEntriesDoc.RootElement.GetProperty("data");
        Assert(entryItems.GetArrayLength() == 1, "Expected entries.list to exclude template files.");
        Assert(entryItems[0].GetProperty("FileName").GetString() == "2026-02-03.md", "Expected only daily entry file in entries.list.");

        var loadTemplateRequest = JsonSerializer.Serialize(new
        {
            action = "templates.load",
            payload = new
            {
                filePath = templatePath
            }
        });
        var loadTemplateResponse = await entry.HandleCommandAsync(loadTemplateRequest);
        using var loadTemplateDoc = JsonDocument.Parse(loadTemplateResponse);
        Assert(loadTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.load.");
        var content = loadTemplateDoc.RootElement.GetProperty("data").GetProperty("Content").GetString() ?? "";
        Assert(content.Contains("## Wins", StringComparison.Ordinal), "Expected template content in templates.load result.");

        var deleteTemplateRequest = JsonSerializer.Serialize(new
        {
            action = "templates.delete",
            payload = new
            {
                filePath = templatePath
            }
        });
        var deleteTemplateResponse = await entry.HandleCommandAsync(deleteTemplateRequest);
        using var deleteTemplateDoc = JsonDocument.Parse(deleteTemplateResponse);
        Assert(deleteTemplateDoc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for templates.delete.");
        Assert(deleteTemplateDoc.RootElement.GetProperty("data").GetBoolean(), "Expected templates.delete to return true.");
    }

    static async Task TestEntrySearchEntriesMatchesRawContentAsync()
    {
        var entry = NewEntry();
        await SeedSearchFixtureEntriesAsync(entry);

        var request = JsonSerializer.Serialize(new
        {
            action = "search.entries",
            payload = new
            {
                query = "common token",
            }
        });

        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);

        Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries.");
        var data = doc.RootElement.GetProperty("data");
        Assert(data.ValueKind == JsonValueKind.Array, "Expected data array from search.entries.");
        Assert(data.GetArrayLength() == 2, "Expected two entries matching query across raw content.");
    }

    static async Task TestEntrySearchEntriesWithoutQueryReturnsAllAsync()
    {
        var entry = NewEntry();
        await SeedSearchFixtureEntriesAsync(entry);

        var request = JsonSerializer.Serialize(new
        {
            action = "search.entries",
            payload = new { }
        });

        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);

        Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for search.entries without query.");
        var data = doc.RootElement.GetProperty("data");
        Assert(data.ValueKind == JsonValueKind.Array, "Expected data array from search.entries.");
        Assert(data.GetArrayLength() == 3, "Expected all seeded markdown entries to be returned when query is omitted.");
    }

    static async Task TestEntrySearchEntriesDateRangeFilterAsync()
    {
        var entry = NewEntry();
        await SeedSearchFixtureEntriesAsync(entry);

        var request = JsonSerializer.Serialize(new
        {
            action = "search.entries",
            payload = new
            {
                startDate = "2026-02-02",
                endDate = "2026-02-28",
            }
        });

        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);

        Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for date-range filtered search.");
        var data = doc.RootElement.GetProperty("data");
        Assert(data.GetArrayLength() == 1, "Expected one result for filtered date range.");
        Assert(data[0].GetProperty("FileName").GetString() == "2026-02-05.md", "Date-range result mismatch.");
    }

    static async Task TestEntrySearchEntriesSectionFilterAsync()
    {
        var entry = NewEntry();
        await SeedSearchFixtureEntriesAsync(entry);

        var request = JsonSerializer.Serialize(new
        {
            action = "search.entries",
            payload = new
            {
                query = "focus area",
                section = "Reflection",
            }
        });

        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);

        Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for section-scoped search.");
        var data = doc.RootElement.GetProperty("data");
        Assert(data.GetArrayLength() == 1, "Expected one section-scoped result.");
        Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Section filter result mismatch.");
    }

    static async Task TestEntrySearchEntriesTagTypeFilterAsync()
    {
        var entry = NewEntry();
        await SeedSearchFixtureEntriesAsync(entry);

        var request = JsonSerializer.Serialize(new
        {
            action = "search.entries",
            payload = new
            {
                tags = new[] { "stress" },
                types = new[] { "!TRIGGER" },
            }
        });

        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);

        Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for fragment tag/type filtered search.");
        var data = doc.RootElement.GetProperty("data");
        Assert(data.GetArrayLength() == 1, "Expected one result for fragment tag/type filters.");
        Assert(data[0].GetProperty("FileName").GetString() == "2026-02-01.md", "Tag/type filter result mismatch.");
    }

    static async Task TestEntrySearchEntriesCheckboxFilterAsync()
    {
        var entry = NewEntry();
        await SeedSearchFixtureEntriesAsync(entry);

        var request = JsonSerializer.Serialize(new
        {
            action = "search.entries",
            payload = new
            {
                @checked = new[] { "med taken" },
                @unchecked = new[] { "drink water" },
            }
        });

        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);

        Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for checkbox filtered search.");
        var data = doc.RootElement.GetProperty("data");
        Assert(data.GetArrayLength() == 2, "Expected OR-style checkbox match across checked/unchecked filters.");
    }

    static async Task TestEntrySearchEntriesRejectsInvalidDateAsync()
    {
        var entry = NewEntry();
        await SeedSearchFixtureEntriesAsync(entry);

        var request = JsonSerializer.Serialize(new
        {
            action = "search.entries",
            payload = new
            {
                startDate = "2026/02/01",
            }
        });

        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);

        Assert(!doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=false for invalid date format.");
        var error = doc.RootElement.GetProperty("error").GetString() ?? "";
        Assert(error.Contains("invalid startdate value", StringComparison.OrdinalIgnoreCase), "Expected invalid startDate error.");
    }

    static Task TestSidecarSearchCliFilteredAsync()
    {
        var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
        var config = NewConfigService(root);
        var dbService = new JournalDatabaseService(config);

        using var session = new DatabaseSessionService(dbService);
        session.SetPassword("vault-pass-123");

        var repo = new SqliteEntryFileRepository(session);
        var entryFiles = new EntryFileService(repo);
        var searchService = new EntrySearchService(repo);
        var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService(), dbService), searchService, config, entryFiles);

        try
        {
            entryFiles.SaveEntry(new EntrySavePayload("""
Date: 2026-02-01
## Summary
common
- [x] med taken
!TRIGGER #stress
matched body
""", Mode: "Overwrite", FileName: "2026-02-01"));
            entryFiles.SaveEntry(new EntrySavePayload("""
Date: 2026-02-05
## Summary
common
- [ ] drink water
!NOTE #daily
non match
""", Mode: "Overwrite", FileName: "2026-02-05"));

            var (exitCode, stdout, stderr) = CaptureConsole(() => cli.RunSearchCommand(
            [
                "common",
                "--start-date", "2026-02-01",
                "--end-date", "2026-02-28",
                "--tag", "stress",
                "--type", "!TRIGGER",
                "--checked", "med taken",
                "--section", "Summary"
            ]));

            Assert(exitCode == 0, "Expected search CLI command to succeed.");
            Assert(string.IsNullOrWhiteSpace(stderr), "Expected no stderr output for successful search CLI command.");
            Assert(stdout.Contains("--- 2026-02-01 ---", StringComparison.Ordinal), "Expected matching entry header in search CLI output.");
            Assert(!stdout.Contains("--- 2026-02-05 ---", StringComparison.Ordinal), "Unexpected non-matching entry in filtered search CLI output.");
        }
        finally
        {
            session.Dispose();
            if (Directory.Exists(root))
                Directory.Delete(root, recursive: true);
        }

        return Task.CompletedTask;
    }

    static Task TestSidecarSearchCliEmptyDataAsync()
    {
        var root = Path.Combine(Path.GetTempPath(), "journal-sidecar-cli-smoke", Guid.NewGuid().ToString("N"));
        var config = NewConfigService(root);
        var dbService = new JournalDatabaseService(config);

        using var session = new DatabaseSessionService(dbService);
        session.SetPassword("vault-pass-123");

        var repo = new SqliteEntryFileRepository(session);
        var entryFiles = new EntryFileService(repo);
        var searchService = new EntrySearchService(repo);
        var cli = new SidecarCli(new VaultStorageService(new VaultCryptoService(), dbService), searchService, config, entryFiles);

        try
        {
            var (exitCode, stdout, _) = CaptureConsole(() => cli.RunSearchCommand([]));

            Assert(exitCode == 0, "Expected search CLI command to return success for empty data store.");
            Assert(stdout.Contains("No journal entries found", StringComparison.OrdinalIgnoreCase), "Expected empty-data guidance message.");
        }
        finally
        {
            session.Dispose();
            if (Directory.Exists(root))
                Directory.Delete(root, recursive: true);
        }

        return Task.CompletedTask;
    }

    static Task TestEntrySavePayloadFileNameDeserializationAsync()
    {
        var json = """{"content":"hello","mode":"Overwrite","fileName":"My Custom Name"}""";
        var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
        var element = JsonSerializer.Deserialize<JsonElement>(json);
        var payload = element.Deserialize<EntrySavePayload>(options);

        Assert(payload is not null, "Payload should not be null.");
        Assert(payload!.Content == "hello", "Content should be deserialized.");
        Assert(payload.Mode == "Overwrite", "Mode should be deserialized.");
        Assert(payload.FileName == "My Custom Name", "FileName should be deserialized from camelCase JSON via JsonElement.");
        Assert(payload.FilePath is null, "FilePath should be null when not provided.");

        return Task.CompletedTask;
    }

    static async Task TestEntrySaveWithFileNameAsync()
    {
        var root = Path.Combine(Path.GetTempPath(), "journal-entry-smoke", Guid.NewGuid().ToString("N"));
        var config = NewConfigService(root);
        var dbService = new JournalDatabaseService(config);

        using var session = new DatabaseSessionService(dbService);
        session.SetPassword("vault-pass-123");

        var repo = new SqliteEntryFileRepository(session);
        var service = new EntryFileService(repo);

        try
        {
            var payload = new EntrySavePayload(
                Content: "# Custom Entry\n\nHello world",
                FilePath: null,
                Mode: "Overwrite",
                FileName: "My Custom Name");

            var result = service.SaveEntry(payload);

            Assert(result.FilePath.StartsWith("db://entry/", StringComparison.OrdinalIgnoreCase), "Expected canonical db://entry path.");
            Assert(result.FilePath.Contains("My%20Custom%20Name.md", StringComparison.OrdinalIgnoreCase), "Expected escaped custom name in db path.");

            var loaded = service.LoadEntry(result.FilePath);
            Assert(loaded.Entry.RawContent.Contains("Hello world", StringComparison.Ordinal), "Stored content should match.");

            var entry = NewEntry();
            var request = JsonSerializer.Serialize(new
            {
                action = "entries.save",
                payload = new
                {
                    content = "# Second Entry",
                    mode = "Overwrite",
                    fileName = "Another Custom Name"
                }
            });

            var response = await entry.HandleCommandAsync(request);
            using var doc = JsonDocument.Parse(response);
            Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for entries.save with fileName.");

            var savedFilePath = doc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? "";
            Assert(savedFilePath.Contains("Another%20Custom%20Name.md", StringComparison.OrdinalIgnoreCase),
                $"Expected file path to contain 'Another%20Custom%20Name.md' but got '{savedFilePath}'.");
        }
        finally
        {
            session.Dispose();
            if (Directory.Exists(root))
                Directory.Delete(root, recursive: true);
        }
    }

    private static async Task<string> SaveEntryForTestAsync(Entry entry, string fileStem, string content)
    {
        var request = JsonSerializer.Serialize(new
        {
            action = "entries.save",
            payload = new
            {
                content,
                mode = "Overwrite",
                fileName = fileStem
            }
        });

        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);
        Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for test seed entries.save.");
        return doc.RootElement.GetProperty("data").GetProperty("FilePath").GetString() ?? "";
    }

    private static async Task<string> LoadEntryForTestAsync(Entry entry, string filePath)
    {
        var request = JsonSerializer.Serialize(new
        {
            action = "entries.load",
            payload = new
            {
                filePath
            }
        });

        var response = await entry.HandleCommandAsync(request);
        using var doc = JsonDocument.Parse(response);
        Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for test entries.load.");
        return doc.RootElement.GetProperty("data").GetProperty("Entry").GetProperty("RawContent").GetString() ?? "";
    }

    private static async Task SeedSearchFixtureEntriesAsync(Entry entry)
    {
        await SaveEntryForTestAsync(entry, "2026-02-01", """
Date: 2026-02-01
## Summary
Alpha line
common token
## Reflection
focus area
- [x] med taken
!TRIGGER #stress
fragment one
""");

        await SaveEntryForTestAsync(entry, "2026-02-05", """
Date: 2026-02-05
## Summary
beta line
COMMON token
## Reflection
other notes
- [ ] drink water
!NOTE #daily
fragment two
""");

        await SaveEntryForTestAsync(entry, "2026-03-01", """
Date: 2026-03-01
## Summary
gamma only
## Reflection
nothing related
!NOTE #other
fragment three
""");
    }
}
