using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.Services.Interfaces;
using ClaudeDo.Ui.ViewModels.Islands;

namespace ClaudeDo.Ui.Tests.ViewModels;

public class NotesEditorViewModelTests
{
    private static readonly DateOnly Today = DateOnly.FromDateTime(DateTime.Today);

    private sealed class FakeNotes : INotesApi
    {
        public readonly List<DailyNoteDto> Store = new();
        private int _seq;

        public Task<List<DailyNoteDto>> ListAsync(DateOnly day) =>
            Task.FromResult(Store.Where(n => n.Date == Iso(day)).ToList());

        public Task<List<DailyNoteDto>> ListBetweenAsync(DateOnly start, DateOnly end) =>
            Task.FromResult(Store
                .Where(n => DateOnly.Parse(n.Date) >= start && DateOnly.Parse(n.Date) <= end)
                .ToList());

        public Task<DailyNoteDto?> AddAsync(DateOnly day, string text)
        {
            var dto = new DailyNoteDto($"id{_seq++}", Iso(day), text, Store.Count, false);
            Store.Add(dto);
            return Task.FromResult<DailyNoteDto?>(dto);
        }

        public Task UpdateAsync(string id, string text)
        {
            var i = Store.FindIndex(n => n.Id == id);
            if (i >= 0) Store[i] = Store[i] with { Text = text };
            return Task.CompletedTask;
        }

        public Task DeleteAsync(string id) { Store.RemoveAll(n => n.Id == id); return Task.CompletedTask; }

        public void Seed(DateOnly day, string text, bool fromAgent = false) =>
            Store.Add(new DailyNoteDto($"id{_seq++}", Iso(day), text, Store.Count, fromAgent));

        private static string Iso(DateOnly d) => d.ToString("yyyy-MM-dd");
    }

    private sealed class ThrowingNotes : INotesApi
    {
        public string ExceptionMessage { get; init; } = "worker offline";
        public Task<List<DailyNoteDto>> ListAsync(DateOnly day) => Task.FromResult(new List<DailyNoteDto>());
        public Task<List<DailyNoteDto>> ListBetweenAsync(DateOnly start, DateOnly end) => Task.FromResult(new List<DailyNoteDto>());
        public Task<DailyNoteDto?> AddAsync(DateOnly day, string text) => throw new Exception(ExceptionMessage);
        public Task UpdateAsync(string id, string text) => Task.CompletedTask;
        public Task DeleteAsync(string id) => Task.CompletedTask;
    }

    private static async Task<(NotesEditorViewModel vm, FakeNotes api)> LoadedAsync(Action<FakeNotes>? seed = null)
    {
        var api = new FakeNotes();
        seed?.Invoke(api);
        var vm = new NotesEditorViewModel(api);
        await vm.LoadAsync();
        return (vm, api);
    }

    private static List<NoteBulletViewModel> Bullets(NotesEditorViewModel vm) =>
        vm.Rows.OfType<NoteBulletViewModel>().ToList();

    [Fact]
    public async Task LoadAsync_groups_notes_by_day_with_today_first()
    {
        var (vm, _) = await LoadedAsync(api =>
        {
            api.Seed(Today.AddDays(-2), "vorgestern");
            api.Seed(Today, "heute");
        });

        var headers = vm.Rows.OfType<NoteDayHeaderViewModel>().ToList();
        Assert.Equal(Today, headers[0].Day);
        Assert.True(headers[0].IsToday);
        Assert.Equal(Today.AddDays(-2), headers[1].Day);
        Assert.Equal("heute", ((NoteBulletViewModel)vm.Rows[1]).Text);
    }

    [Fact]
    public async Task LoadAsync_keeps_todays_header_when_there_is_nothing_yet()
    {
        var (vm, _) = await LoadedAsync();

        Assert.Single(vm.Rows);
        Assert.True(vm.IsEmpty);
    }

    [Fact]
    public async Task AddBullet_files_the_note_under_today_and_clears_the_box()
    {
        var (vm, api) = await LoadedAsync(api => api.Seed(Today.AddDays(-1), "gestern"));

        vm.NewBulletText = "Standup vorbereitet";
        await vm.AddBulletCommand.ExecuteAsync(null);

        Assert.Equal("Standup vorbereitet", ((NoteBulletViewModel)vm.Rows[1]).Text);
        Assert.Equal("", vm.NewBulletText);
        Assert.False(vm.IsEmpty);
        Assert.Equal(2, api.Store.Count);
    }

    [Fact]
    public async Task AddBullet_WhenApiThrows_RaisesErrorReported_AndKeepsDraftText()
    {
        var api = new ThrowingNotes();
        var vm = new NotesEditorViewModel(api);
        await vm.LoadAsync();

        string? reportedError = null;
        vm.ErrorReported += msg => reportedError = msg;

        vm.NewBulletText = "Standup vorbereitet";
        await vm.AddBulletCommand.ExecuteAsync(null);

        Assert.Equal(api.ExceptionMessage, reportedError);
        Assert.Empty(Bullets(vm));
        Assert.Equal("Standup vorbereitet", vm.NewBulletText);
    }

    [Fact]
    public async Task SplitBullet_saves_the_edit_then_opens_an_empty_row_below_and_focuses_it()
    {
        var (vm, api) = await LoadedAsync(api => api.Seed(Today, "erste"));
        NoteBulletViewModel? focused = null;
        vm.FocusRequested += b => focused = b;

        var first = Bullets(vm)[0];
        first.Text = "erste, bearbeitet";
        await vm.SplitBulletCommand.ExecuteAsync(first);

        var rows = Bullets(vm);
        Assert.Equal(2, rows.Count);
        Assert.Same(rows[1], focused);
        Assert.True(rows[1].IsNew);
        Assert.Equal("erste, bearbeitet", api.Store[0].Text);
        // The empty row is UI-only until it gets text, so Enter on a blank line leaves nothing behind.
        Assert.Single(api.Store);
    }

    [Fact]
    public async Task SplitBullet_on_an_emptied_note_deletes_it_and_adds_no_row()
    {
        var (vm, api) = await LoadedAsync(api => api.Seed(Today, "weg damit"));

        var only = Bullets(vm)[0];
        only.Text = "   ";
        await vm.SplitBulletCommand.ExecuteAsync(only);

        Assert.Empty(Bullets(vm));
        Assert.Empty(api.Store);
        Assert.True(vm.IsEmpty);
    }

    [Fact]
    public async Task CommitBullet_persists_a_new_row_and_gives_it_an_id()
    {
        var (vm, api) = await LoadedAsync(api => api.Seed(Today, "erste"));
        await vm.SplitBulletCommand.ExecuteAsync(Bullets(vm)[0]);

        var fresh = Bullets(vm)[1];
        fresh.Text = "zweite";
        await vm.CommitBulletCommand.ExecuteAsync(fresh);

        Assert.False(fresh.IsNew);
        Assert.Equal(2, api.Store.Count);
        Assert.Equal("zweite", api.Store[1].Text);
    }

    [Fact]
    public async Task DeleteBullet_removes_it_and_focuses_the_note_above()
    {
        var (vm, api) = await LoadedAsync(api => { api.Seed(Today, "eins"); api.Seed(Today, "zwei"); });
        NoteBulletViewModel? focused = null;
        vm.FocusRequested += b => focused = b;

        var second = Bullets(vm)[1];
        await vm.DeleteBulletCommand.ExecuteAsync(second);

        Assert.Single(Bullets(vm));
        Assert.Equal("eins", focused?.Text);
        Assert.Single(api.Store);
    }

    [Fact]
    public async Task DeleteBullet_on_the_first_note_of_the_day_does_not_reach_into_the_header()
    {
        var (vm, _) = await LoadedAsync(api => api.Seed(Today, "einzige"));
        var focusCalls = 0;
        vm.FocusRequested += _ => focusCalls++;

        await vm.DeleteBulletCommand.ExecuteAsync(Bullets(vm)[0]);

        Assert.Equal(0, focusCalls);
        Assert.True(vm.IsEmpty);
    }

    [Fact]
    public async Task CommitBullet_EmptyText_RemovesFromStoreAndList()
    {
        var (vm, api) = await LoadedAsync(api => api.Seed(Today, "weg damit"));

        var only = Bullets(vm)[0];
        only.Text = "   ";
        await vm.CommitBulletCommand.ExecuteAsync(only);

        Assert.Empty(Bullets(vm));
        Assert.Empty(api.Store);
    }

    [Fact]
    public async Task CommitBullet_NonEmptyText_PersistsEdit()
    {
        var (vm, api) = await LoadedAsync(api => api.Seed(Today, "original"));

        var only = Bullets(vm)[0];
        only.Text = "geaendert";
        await vm.CommitBulletCommand.ExecuteAsync(only);

        Assert.Single(Bullets(vm));
        Assert.Equal("geaendert", api.Store[0].Text);
    }

    [Fact]
    public async Task Agent_written_notes_are_marked_so_the_view_can_flag_them()
    {
        var (vm, _) = await LoadedAsync(api =>
        {
            api.Seed(Today, "von mir");
            api.Seed(Today, "von Claude", fromAgent: true);
        });

        Assert.False(Bullets(vm)[0].FromAgent);
        Assert.True(Bullets(vm)[1].FromAgent);
    }
}
