using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.Models;
using ytLive.Services;
using ytLive.ViewModels;

namespace ytLive.Tests;

public class SocialsDialogViewModelTests
{
    private sealed class FakeValidator : ISocialValidator
    {
        public int LookupCount { get; private set; }

        public Task<string?> ResolveFediverseSoftwareAsync(string domain, CancellationToken ct)
            => Task.FromResult<string?>("mastodon");

        public Task<SocialLookupResult> LookupAsync(SocialService service, string handleOrUrl, CancellationToken ct)
        {
            LookupCount++;
            if (handleOrUrl.StartsWith('@') && handleOrUrl.IndexOf('@', 1) > 0)
            {
                var at = handleOrUrl.IndexOf('@', 1);
                var user = handleOrUrl[1..at];
                var domain = handleOrUrl[(at + 1)..];
                return Task.FromResult(new SocialLookupResult
                {
                    Success = true,
                    Handle = handleOrUrl,
                    ProfileUrl = $"https://{domain}/@{user}",
                    FediverseSoftware = "peertube",
                });
            }
            var handle = handleOrUrl.Trim().TrimStart('@');
            if (handle == "doesnotexist")
                return Task.FromResult(new SocialLookupResult { Error = "404 for 'doesnotexist'." });
            return Task.FromResult(new SocialLookupResult
            {
                Success = true,
                Handle = handle,
                ProfileUrl = SocialServiceIcons.CanonicalUrlFor(service, handle),
            });
        }
    }

    /// <summary>Holds the lookup open so a test can cancel mid-flight and then
    /// resolve it — proving a dismissed dialog never applies the result.</summary>
    private sealed class BlockingValidator : ISocialValidator
    {
        public TaskCompletionSource<SocialLookupResult> Gate { get; } = new();

        public Task<SocialLookupResult> LookupAsync(SocialService service, string handleOrUrl, CancellationToken ct)
            => Gate.Task;

        public Task<string?> ResolveFediverseSoftwareAsync(string domain, CancellationToken ct)
            => Task.FromResult<string?>(null);
    }

    private sealed class SignInFake
    {
        public int Calls { get; private set; }
        public Task<YouTubeChannel?> Next()
        {
            Calls++;
            return Task.FromResult<YouTubeChannel?>(
                new YouTubeChannel { ChannelId = "UCabc", DisplayName = "My Channel" });
        }
    }

    private int _signOutCalls;
    private Task SignOut() { _signOutCalls++; return Task.CompletedTask; }

    private SocialsDialogViewModel Create(
        ISocialValidator? validator = null,
        SignInFake? signIn = null,
        YouTubeChannel? account = null,
        bool isPremium = false,
        SocialsConfig? current = null)
    {
        return new SocialsDialogViewModel(
            validator ?? new FakeValidator(),
            signIn != null ? signIn.Next : () => Task.FromResult<YouTubeChannel?>(null),
            SignOut,
            account,
            isPremium,
            current);
    }

    // ── ONE integration test: the full dialog-VM flow with fakes ──

    [Fact]
    public async Task FullFlow_Freemium_SignInAddDeleteSignOutPersistence()
    {
        var validator = new FakeValidator();
        var signIn = new SignInFake();
        var vm = Create(validator, signIn);

        // Gate: signed out, freemium → row 0 is the sign-in prompt, rows 2-5 locked.
        Assert.True(vm.ShowSignInBanner);
        Assert.Equal(6, vm.Slots.Count);
        Assert.True(vm.Slots[0].IsSignIn);
        Assert.False(vm.Slots[0].IsLocked);
        Assert.False(vm.Slots[1].IsLocked);
        Assert.True(vm.Slots[2].IsLocked);
        Assert.True(vm.Slots[5].IsLocked);
        Assert.True(vm.CanSave);

        // Row 1: add a validated X handle. Save is blocked while input is unvalidated.
        vm.StartEdit(1);
        Assert.True(vm.Slots[1].IsEditing);
        vm.Slots[1].EditText = "x.com/creator";
        Assert.False(vm.CanSave);
        vm.ConfirmEdit(1);
        Assert.Equal(1, validator.LookupCount);
        Assert.Equal(SocialService.X, vm.Slots[1].Service);
        Assert.Equal("creator", vm.Slots[1].Handle);
        Assert.True(vm.Slots[1].IsFilled);
        Assert.False(vm.Slots[1].IsEditing);
        Assert.True(vm.CanSave);

        // Rows 2-5 are locked on the free tier — starting an edit there is blocked.
        vm.StartEdit(2);
        Assert.False(vm.Slots[2].IsEditing);
        Assert.True(vm.Slots[2].IsLocked);

        // Delete the X entry: the slot empties and unlocks nothing else.
        await vm.DeleteSlotAsync(1);
        Assert.False(vm.Slots[1].IsFilled);
        Assert.False(vm.Slots[1].IsLocked);
        Assert.True(vm.Slots[2].IsLocked);

        // Delete row 0 while signed out is a no-op.
        await vm.DeleteSlotAsync(0);
        Assert.Equal(0, _signOutCalls);

        // Sign in: row 0 fills with the channel; the X slot is still free.
        vm.SignInCommand.Execute(null);
        Assert.Equal(1, signIn.Calls);
        Assert.True(vm.IsSignedIn);
        Assert.False(vm.Slots[0].IsSignIn);
        Assert.Equal("My Channel", vm.Slots[0].Handle);
        Assert.False(vm.ShowSignInBanner);

        // Fill row 1 again and save: YouTube-first ordering, bar enabled.
        vm.StartEdit(1);
        vm.Slots[1].EditText = "twitch.tv/streamer";
        vm.ConfirmEdit(1);
        vm.SaveCommand.Execute(null);
        Assert.NotNull(vm.CommittedEntries);
        Assert.Equal(2, vm.CommittedEntries!.Count);
        Assert.Equal(SocialService.YouTube, vm.CommittedEntries[0].Service);
        Assert.Equal("My Channel", vm.CommittedEntries[0].Handle);
        Assert.Equal(SocialService.Twitch, vm.CommittedEntries[1].Service);
        Assert.Equal("streamer", vm.CommittedEntries[1].Handle);

        // Sign out via the row-0 delete: action called once, row 0 back to the gate.
        await vm.DeleteSlotAsync(0);
        Assert.Equal(1, _signOutCalls);
        Assert.False(vm.IsSignedIn);
        Assert.True(vm.Slots[0].IsSignIn);

        // Full persistence roundtrip through the store.
        var config = vm.BuildSocialsConfig();
        Assert.NotNull(config);
        Assert.True(config!.BarEnabled);
        Assert.Equal(2, config.Entries.Count);

        var path = Path.Combine(Path.GetTempPath(), $"ytLlive-social-flow-{System.Guid.NewGuid():N}.db");
        SqliteConnection.ClearAllPools();
        try
        {
            using (var store = new LayoutStore(path))
                store.Save(new[] { new Scene { Name = "Live" } }, null, config);

            using (var store = new LayoutStore(path))
            {
                store.Load();
                Assert.NotNull(store.Socials);
                Assert.Equal(2, store.Socials!.Entries.Count);
                Assert.Equal(SocialService.YouTube, store.Socials.Entries[0].Service);
                Assert.Equal(SocialService.Twitch, store.Socials.Entries[1].Service);
                Assert.True(store.Socials.BarEnabled);
            }
        }
        finally
        {
            SqliteConnection.ClearAllPools();
            if (File.Exists(path)) File.Delete(path);
        }
    }

    // ── Unit tests ──

    [Fact]
    public void Freemium_LocksSlotsBeyondTwo_PremiumUnlocksAll()
    {
        var free = Create();
        Assert.True(free.Slots[2].IsLocked);
        Assert.True(free.Slots[5].IsLocked);
        Assert.False(free.Slots[1].IsLocked);

        var premium = Create(isPremium: true);
        Assert.All(premium.Slots, s => Assert.False(s.IsLocked));
        Assert.False(premium.Slots[5].IsLocked);
    }

    [Fact]
    public void ValidationFailure_LeavesSlotEditableAndBlocksSave()
    {
        var validator = new FakeValidator();
        var vm = Create(validator);

        vm.StartEdit(1);
        vm.Slots[1].EditText = "x.com/doesnotexist";
        vm.ConfirmEdit(1);

        Assert.True(vm.Slots[1].IsEditing);
        Assert.True(vm.Slots[1].HasError);
        Assert.False(vm.Slots[1].IsFilled);
        Assert.False(vm.CanSave);

        vm.Slots[1].EditText = "github.com/dev";
        vm.ConfirmEdit(1);
        Assert.Equal(SocialService.GitHub, vm.Slots[1].Service);
        Assert.False(vm.Slots[1].HasError);
        Assert.True(vm.CanSave);
    }

    [Fact]
    public void EmptyEdit_CancelsEditing()
    {
        var vm = Create();
        vm.StartEdit(1);
        vm.Slots[1].EditText = "x.com/user";
        vm.ConfirmEdit(1);
        Assert.True(vm.Slots[1].IsFilled);

        vm.StartEdit(1);
        vm.Slots[1].EditText = "";
        vm.ConfirmEdit(1);
        Assert.False(vm.Slots[1].IsEditing);
        Assert.True(vm.Slots[1].IsFilled);
    }

    [Fact]
    public void SeedsFromCurrent_YouTubeBecomesAccountRow()
    {
        var current = new SocialsConfig { BarEnabled = false };
        current.Entries.Add(new SocialEntry
        {
            Service = SocialService.YouTube,
            Handle = "stale-yt-handle",
            ProfileUrl = "https://www.youtube.com/@stale-yt-handle",
        });
        current.Entries.Add(new SocialEntry
        {
            Service = SocialService.Twitch,
            Handle = "oldstreamer",
            ProfileUrl = "https://www.twitch.tv/oldstreamer",
        });

        var vm = Create(account: new YouTubeChannel { ChannelId = "UC123", DisplayName = "Fresh Channel" }, current: current);

        Assert.False(vm.BarEnabled);
        Assert.Equal("Fresh Channel", vm.Slots[0].Handle);
        Assert.Equal(SocialService.Twitch, vm.Slots[1].Service);
        Assert.Equal("oldstreamer", vm.Slots[1].Handle);
        Assert.True(vm.Slots[2].IsLocked);

        vm.SaveCommand.Execute(null);
        Assert.Equal(2, vm.CommittedEntries!.Count);
        Assert.Equal("Fresh Channel", vm.CommittedEntries[0].Handle);
        Assert.Equal(SocialService.Twitch, vm.CommittedEntries[1].Service);
        Assert.False(vm.BuildSocialsConfig()!.BarEnabled);
    }

    [Fact]
    public void EmptySave_BuildsNullConfig()
    {
        var vm = Create();
        Assert.True(vm.CanSave);
        vm.SaveCommand.Execute(null);
        Assert.NotNull(vm.CommittedEntries);
        Assert.Empty(vm.CommittedEntries!);
        Assert.Null(vm.BuildSocialsConfig());
    }

    [Fact]
    public async Task SignOut_ResetsRowZeroToGate()
    {
        var signIn = new SignInFake();
        var vm = Create(signIn: signIn, account: new YouTubeChannel { ChannelId = "UC1", DisplayName = "Ch" });
        Assert.True(vm.IsSignedIn);

        await vm.DeleteSlotAsync(0);

        Assert.Equal(1, _signOutCalls);
        Assert.False(vm.IsSignedIn);
        Assert.True(vm.Slots[0].IsSignIn);
        Assert.True(vm.ShowSignInBanner);
    }

    [Fact]
    public void FediverseHandle_ValidatesAndKeepsFullHandle()
    {
        var vm = Create();
        vm.StartEdit(1);
        vm.Slots[1].EditText = "@creator@instance.tube";
        vm.ConfirmEdit(1);

        Assert.Equal(SocialService.Fediverse, vm.Slots[1].Service);
        Assert.Equal("@creator@instance.tube", vm.Slots[1].Handle);
        Assert.Equal("https://instance.tube/@creator", vm.Slots[1].ProfileUrl);
        Assert.Equal("peertube", vm.Slots[1].FediverseSoftware);
        Assert.Equal(SocialServiceIcons.LogoDataForFediverse("peertube"), vm.Slots[1].LogoData);
        Assert.True(vm.Slots[1].IsFilled);
        Assert.False(vm.Slots[1].IsEditing);
        Assert.True(vm.CanSave);
    }

    [Fact]
    public void FediverseEntry_CommitsSoftwareName()
    {
        var vm = Create();
        vm.StartEdit(1);
        vm.Slots[1].EditText = "@creator@instance.tube";
        vm.ConfirmEdit(1);
        vm.SaveCommand.Execute(null);

        Assert.NotNull(vm.CommittedEntries);
        var entry = Assert.Single(vm.CommittedEntries!);
        Assert.Equal(SocialService.Fediverse, entry.Service);
        Assert.Equal("peertube", entry.FediverseSoftware);
        Assert.Equal(SocialServiceIcons.LogoDataForFediverse("peertube"), entry.LogoData);
        var config = vm.BuildSocialsConfig();
        Assert.NotNull(config);
        Assert.Equal(SocialServiceIcons.LogoDataForFediverse("peertube"), Assert.Single(config.Entries).LogoData);
    }

    [Fact]
    public void Cancel_AbortsInFlightValidation_WithoutMutatingSlot()
    {
        var validator = new BlockingValidator();
        var vm = Create(validator);

        vm.StartEdit(1);
        vm.Slots[1].EditText = "x.com/creator";
        vm.ConfirmEdit(1);
        Assert.True(vm.Slots[1].IsValidating);

        vm.Cancel();
        // The lookup resolves with success AFTER dismissal — it must not apply.
        validator.Gate.TrySetResult(new SocialLookupResult
        {
            Success = true,
            Handle = "creator",
            ProfileUrl = "https://x.com/creator",
        });

        Assert.False(vm.Slots[1].IsFilled);
        Assert.False(vm.Slots[1].HasError);
        Assert.False(vm.Slots[1].IsValidating);
        Assert.True(vm.Slots[1].IsEditing);
    }

    [Fact]
    public void ReSubmittingIdenticalFailedInput_DoesNotRelookup()
    {
        var validator = new FakeValidator();
        var vm = Create(validator);

        vm.StartEdit(1);
        vm.Slots[1].EditText = "x.com/doesnotexist";
        vm.ConfirmEdit(1);
        Assert.Equal(1, validator.LookupCount);
        Assert.True(vm.Slots[1].HasError);

        // LostFocus firing on Cancel re-submits the same failed text — no second lookup.
        vm.ConfirmEdit(1);
        Assert.Equal(1, validator.LookupCount);
        Assert.True(vm.Slots[1].HasError);

        // Editing the text re-enables validation.
        vm.Slots[1].EditText = "github.com/dev";
        vm.ConfirmEdit(1);
        Assert.Equal(2, validator.LookupCount);
        Assert.False(vm.Slots[1].HasError);
        Assert.True(vm.Slots[1].IsFilled);
    }

    [Fact]
    public void ReEditingUnchangedHandle_ClosesBox_NoRelookup()
    {
        var validator = new FakeValidator();
        var vm = Create(validator);

        vm.StartEdit(1);
        vm.Slots[1].EditText = "x.com/creator";
        vm.ConfirmEdit(1);
        Assert.Equal(1, validator.LookupCount);
        Assert.True(vm.Slots[1].IsFilled);

        vm.StartEdit(1);
        Assert.True(vm.Slots[1].IsEditing);
        vm.ConfirmEdit(1);

        Assert.Equal(1, validator.LookupCount);
        Assert.False(vm.Slots[1].IsEditing);
        Assert.True(vm.Slots[1].IsFilled);
    }
}
