using System.Reflection;
using OF_DL.Models.Config;
using OF_DL.Models.Downloads;
using OF_DL.Models;
using PostEntities = OF_DL.Models.Entities.Posts;
using PurchasedEntities = OF_DL.Models.Entities.Purchased;
using MessageEntities = OF_DL.Models.Entities.Messages;
using OF_DL.Services;

namespace OF_DL.Tests.Services;

public class DownloadServiceTests
{
    [Fact]
    public async Task ProcessMediaDownload_RenamesServerFileAndUpdatesDb_WhenNotDownloadedButServerFileExists()
    {
        using TempFolder temp = new();
        string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
        string path = "/Posts/Free";
        string url = "https://example.com/image.jpg";
        string serverFilename = "server";
        string resolvedFilename = "custom";
        string serverFilePath = $"{folder}{path}/{serverFilename}.jpg";
        Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException());
        await File.WriteAllTextAsync(serverFilePath, "abc");

        MediaTrackingDbService dbService = new() { CheckDownloadedResult = false };
        FakeConfigService configService = new(new Config { ShowScrapeSize = false });
        DownloadService service = CreateService(configService, dbService);
        ProgressRecorder progress = new();

        bool isNew = await service.ProcessMediaDownload(folder, 1, "Posts", url, path, serverFilename,
            resolvedFilename, ".jpg", progress);

        string renamedPath = $"{folder}{path}/{resolvedFilename}.jpg";
        Assert.False(isNew);
        Assert.False(File.Exists(serverFilePath));
        Assert.True(File.Exists(renamedPath));
        Assert.NotNull(dbService.LastUpdateMedia);
        Assert.Equal($"{folder}{path}", dbService.LastUpdateMedia.Value.directory);
        Assert.Equal("custom.jpg", dbService.LastUpdateMedia.Value.filename);
        Assert.Equal(new FileInfo(renamedPath).Length, dbService.LastUpdateMedia.Value.size);
        Assert.Equal(1, progress.Total);
    }

    [Fact]
    public async Task ProcessMediaDownload_RenamesExistingFile_WhenDownloadedAndCustomFormatEnabled()
    {
        using TempFolder temp = new();
        string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
        string path = "/Posts/Free";
        string url = "https://example.com/image.jpg";
        string serverFilename = "server";
        string resolvedFilename = "custom";
        string serverFilePath = $"{folder}{path}/{serverFilename}.jpg";
        Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException());
        await File.WriteAllTextAsync(serverFilePath, "abc");

        MediaTrackingDbService dbService = new() { CheckDownloadedResult = true, StoredFileSize = 123 };
        FakeConfigService configService =
            new(new Config { ShowScrapeSize = false, RenameExistingFilesWhenCustomFormatIsSelected = true });
        DownloadService service = CreateService(configService, dbService);
        ProgressRecorder progress = new();

        bool isNew = await service.ProcessMediaDownload(folder, 1, "Posts", url, path, serverFilename,
            resolvedFilename, ".jpg", progress);

        string renamedPath = $"{folder}{path}/{resolvedFilename}.jpg";
        Assert.False(isNew);
        Assert.False(File.Exists(serverFilePath));
        Assert.True(File.Exists(renamedPath));
        Assert.NotNull(dbService.LastUpdateMedia);
        Assert.Equal("custom.jpg", dbService.LastUpdateMedia.Value.filename);
        Assert.Equal(123, dbService.LastUpdateMedia.Value.size);
        Assert.Equal(1, progress.Total);
    }

    [Fact]
    public async Task GetDecryptionInfo_UsesOfdlWhenCdmMissing()
    {
        StaticApiService apiService = new();
        DownloadService service =
            CreateService(new FakeConfigService(new Config()), new MediaTrackingDbService(), apiService);

        (string decryptionKey, DateTime lastModified, double? mpdDurationSeconds)? result = await service.GetDecryptionInfo(
            "https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
            true, false);

        Assert.NotNull(result);
        Assert.Equal("ofdl-key", result.Value.decryptionKey);
        Assert.Equal(apiService.LastModifiedToReturn, result.Value.lastModified);
        Assert.True(apiService.OfdlCalled);
        Assert.False(apiService.CdmCalled);
    }

    [Fact]
    public async Task GetDecryptionInfo_UsesCdmWhenAvailable()
    {
        StaticApiService apiService = new();
        DownloadService service =
            CreateService(new FakeConfigService(new Config()), new MediaTrackingDbService(), apiService);

        (string decryptionKey, DateTime lastModified, double? mpdDurationSeconds)? result = await service.GetDecryptionInfo(
            "https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
            false, false);

        Assert.NotNull(result);
        Assert.Equal("cdm-key", result.Value.decryptionKey);
        Assert.Equal(apiService.LastModifiedToReturn, result.Value.lastModified);
        Assert.True(apiService.CdmCalled);
        Assert.False(apiService.OfdlCalled);
    }

    [Fact]
    public async Task FinalizeDrmDownload_DoesNotDeleteFile_WhenCustomPathMatchesTempPath()
    {
        using TempFolder temp = new();
        string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
        string path = "/Posts/Free";
        string filename = "video";
        string customFileName = "video_source";
        string tempFilename = $"{folder}{path}/{filename}_source.mp4";
        Directory.CreateDirectory(Path.GetDirectoryName(tempFilename) ?? throw new InvalidOperationException());
        await File.WriteAllTextAsync(tempFilename, "abc");

        MediaTrackingDbService dbService = new();
        DownloadService service = CreateService(new FakeConfigService(new Config { ShowScrapeSize = false }), dbService);
        ProgressRecorder progress = new();

        MethodInfo? finalizeMethod = typeof(DownloadService).GetMethod("FinalizeDrmDownload",
            BindingFlags.Instance | BindingFlags.NonPublic);
        Assert.NotNull(finalizeMethod);

        object? resultObject = finalizeMethod.Invoke(service,
        [
            tempFilename, DateTime.UtcNow, folder, path, customFileName, filename, 1L, "Posts", progress
        ]);

        bool result = await Assert.IsType<Task<bool>>(resultObject!);
        Assert.True(result);
        Assert.True(File.Exists(tempFilename));
        Assert.NotNull(dbService.LastUpdateMedia);
        Assert.Equal("video_source.mp4", dbService.LastUpdateMedia.Value.filename);
    }

    [Fact]
    public async Task DownloadHighlights_ReturnsZeroWhenNoMedia()
    {
        StaticApiService apiService = new() { MediaToReturn = new Dictionary<long, string>() };
        DownloadService service =
            CreateService(new FakeConfigService(new Config()), new MediaTrackingDbService(), apiService);

        DownloadResult result = await service.DownloadHighlights("user", 1, "/tmp/creator", new HashSet<long>(),
            new ProgressRecorder());

        Assert.Equal(0, result.TotalCount);
        Assert.Equal(0, result.NewDownloads);
        Assert.Equal(0, result.ExistingDownloads);
        Assert.Equal("Highlights", result.MediaType);
        Assert.True(result.Success);
    }

    [Fact]
    public async Task DownloadHighlights_CountsExistingWhenAlreadyDownloaded()
    {
        using TempFolder temp = new();
        string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
        StaticApiService apiService = new()
        {
            MediaToReturn = new Dictionary<long, string>
            {
                { 1, "https://example.com/one.jpg" }, { 2, "https://example.com/two.jpg" }
            }
        };
        MediaTrackingDbService dbService = new() { CheckDownloadedResult = true };
        FakeConfigService configService = new(new Config { ShowScrapeSize = false });
        ProgressRecorder progress = new();
        DownloadService service = CreateService(configService, dbService, apiService);

        DownloadResult result = await service.DownloadHighlights("user", 1, folder, new HashSet<long>(), progress);

        Assert.Equal(2, result.TotalCount);
        Assert.Equal(0, result.NewDownloads);
        Assert.Equal(2, result.ExistingDownloads);
        Assert.Equal("Highlights", result.MediaType);
        Assert.True(result.Success);
        Assert.Equal(2, progress.Total);
    }

    [Fact]
    public async Task DownloadFreePosts_UsesDefaultFilenameWhenNoGlobalOrCreatorFormatIsDefined()
    {
        using TempFolder temp = new();
        string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
        const string serverFilename = "server-name";
        string existingFilePath = $"{folder}/Posts/Free/Images/{serverFilename}.jpg";
        Directory.CreateDirectory(Path.GetDirectoryName(existingFilePath) ?? throw new InvalidOperationException());
        await File.WriteAllTextAsync(existingFilePath, "abc");

        Config config = new()
        {
            ShowScrapeSize = false,
            PostFileNameFormat = "",
            CreatorConfigs = new Dictionary<string, CreatorConfig>
            {
                ["creator"] = new() { PostFileNameFormat = "" }
            }
        };

        MediaTrackingDbService dbService = new() { CheckDownloadedResult = false };
        DownloadService service = CreateService(new FakeConfigService(config), dbService);
        ProgressRecorder progress = new();
        PostEntities.PostCollection posts = new()
        {
            Posts = new Dictionary<long, string> { { 1, $"https://example.com/{serverFilename}.jpg" } }
        };

        DownloadResult result = await service.DownloadFreePosts("creator", 1, folder, new Dictionary<string, long>(),
            false, false, posts, progress);

        Assert.Equal(1, result.TotalCount);
        Assert.Equal(0, result.NewDownloads);
        Assert.Equal(1, result.ExistingDownloads);
        Assert.NotNull(dbService.LastUpdateMedia);
        Assert.Equal($"{serverFilename}.jpg", dbService.LastUpdateMedia.Value.filename);
        Assert.Equal(1, progress.Total);
    }

    [Fact]
    public async Task DownloadFreePosts_UsesGlobalCustomFormatWhenCreatorCustomFormatNotDefined()
    {
        using TempFolder temp = new();
        string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
        const string serverFilename = "server-name";
        string existingFilePath = $"{folder}/Posts/Free/Images/{serverFilename}.jpg";
        Directory.CreateDirectory(Path.GetDirectoryName(existingFilePath) ?? throw new InvalidOperationException());
        await File.WriteAllTextAsync(existingFilePath, "abc");

        Config config = new()
        {
            ShowScrapeSize = false,
            PostFileNameFormat = "global-custom-name",
            CreatorConfigs = new Dictionary<string, CreatorConfig>
            {
                ["creator"] = new() { PostFileNameFormat = "" }
            }
        };

        MediaTrackingDbService dbService = new() { CheckDownloadedResult = false };
        DownloadService service = CreateService(new FakeConfigService(config), dbService,
            fileNameService: new DeterministicFileNameService());
        ProgressRecorder progress = new();
        PostEntities.PostCollection posts = CreatePostCollection(1, serverFilename);

        DownloadResult result = await service.DownloadFreePosts("creator", 1, folder, new Dictionary<string, long>(),
            false, false, posts, progress);

        string renamedPath = $"{folder}/Posts/Free/Images/global-custom-name.jpg";
        Assert.Equal(1, result.TotalCount);
        Assert.Equal(0, result.NewDownloads);
        Assert.Equal(1, result.ExistingDownloads);
        Assert.False(File.Exists(existingFilePath));
        Assert.True(File.Exists(renamedPath));
        Assert.NotNull(dbService.LastUpdateMedia);
        Assert.Equal("global-custom-name.jpg", dbService.LastUpdateMedia.Value.filename);
        Assert.Equal(1, progress.Total);
    }

    [Fact]
    public async Task DownloadFreePosts_UsesCreatorCustomFormatWhenGlobalAndCreatorFormatsAreDefined()
    {
        using TempFolder temp = new();
        string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
        const string serverFilename = "server-name";
        string existingFilePath = $"{folder}/Posts/Free/Images/{serverFilename}.jpg";
        Directory.CreateDirectory(Path.GetDirectoryName(existingFilePath) ?? throw new InvalidOperationException());
        await File.WriteAllTextAsync(existingFilePath, "abc");

        Config config = new()
        {
            ShowScrapeSize = false,
            PostFileNameFormat = "global-custom-name",
            CreatorConfigs = new Dictionary<string, CreatorConfig>
            {
                ["creator"] = new() { PostFileNameFormat = "creator-custom-name" }
            }
        };

        MediaTrackingDbService dbService = new() { CheckDownloadedResult = false };
        DownloadService service = CreateService(new FakeConfigService(config), dbService,
            fileNameService: new DeterministicFileNameService());
        ProgressRecorder progress = new();
        PostEntities.PostCollection posts = CreatePostCollection(1, serverFilename);

        DownloadResult result = await service.DownloadFreePosts("creator", 1, folder, new Dictionary<string, long>(),
            false, false, posts, progress);

        string renamedPath = $"{folder}/Posts/Free/Images/creator-custom-name.jpg";
        Assert.Equal(1, result.TotalCount);
        Assert.Equal(0, result.NewDownloads);
        Assert.Equal(1, result.ExistingDownloads);
        Assert.False(File.Exists(existingFilePath));
        Assert.True(File.Exists(renamedPath));
        Assert.NotNull(dbService.LastUpdateMedia);
        Assert.Equal("creator-custom-name.jpg", dbService.LastUpdateMedia.Value.filename);
        Assert.Equal(1, progress.Total);
    }

    [Fact]
    public async Task DownloadPaidPosts_AppliesPaidCustomFormatForDrm_WhenAuthorExistsButFromUserMissing()
    {
        using TempFolder temp = new();
        string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
        const string customName = "paid-custom-name";
        const string drmBaseFilename = "video-file";
        string basePath = $"{folder}/Posts/Paid/Videos";
        Directory.CreateDirectory(basePath);
        await File.WriteAllTextAsync($"{basePath}/{customName}.mp4", "custom");
        await File.WriteAllTextAsync($"{basePath}/{drmBaseFilename}_source.mp4", "server");

        Config config = new() { ShowScrapeSize = false, PaidPostFileNameFormat = customName, PostFileNameFormat = "" };

        MediaTrackingDbService dbService = new() { CheckDownloadedResult = false };
        StaticApiService apiService = new();
        FakeAuthService authService = new()
        {
            CurrentAuth = new Auth { Cookie = "sess=test;", UserAgent = "unit-test-agent" }
        };

        DownloadService service = CreateService(new FakeConfigService(config), dbService,
            apiService, new DeterministicFileNameService(), authService);
        ProgressRecorder progress = new();
        PurchasedEntities.PaidPostCollection posts = CreatePaidPostCollectionForDrm(1,
            $"https://cdn3.onlyfans.com/dash/files/{drmBaseFilename}.mpd,policy,signature,kvp,1,2");

        DownloadResult result = await service.DownloadPaidPosts("creator", 1, folder, new Dictionary<string, long>(),
            false, false, posts, progress);

        Assert.Equal(1, result.TotalCount);
        Assert.Equal(0, result.NewDownloads);
        Assert.Equal(1, result.ExistingDownloads);
        Assert.NotNull(dbService.LastUpdateMedia);
        Assert.Equal($"{customName}.mp4", dbService.LastUpdateMedia.Value.filename);
        Assert.Equal(1, progress.Total);
    }

    private static DownloadService CreateService(FakeConfigService configService, MediaTrackingDbService dbService,
        StaticApiService? apiService = null, IFileNameService? fileNameService = null,
        IAuthService? authService = null) =>
        new(authService ?? new FakeAuthService(), configService, dbService,
            fileNameService ?? new FakeFileNameService(),
            apiService ?? new StaticApiService());

    private static PostEntities.PostCollection CreatePostCollection(long mediaId, string serverFilename)
    {
        PostEntities.Medium media = new() { Id = mediaId };
        PostEntities.ListItem post = new()
        {
            Id = 10,
            PostedAt = new DateTime(2024, 1, 1),
            Author = new OF_DL.Models.Entities.Common.Author { Id = 99 },
            Media = [media]
        };

        return new PostEntities.PostCollection
        {
            Posts = new Dictionary<long, string> { { mediaId, $"https://example.com/{serverFilename}.jpg" } },
            PostMedia = [media],
            PostObjects = [post]
        };
    }

    private static PurchasedEntities.PaidPostCollection CreatePaidPostCollectionForDrm(long mediaId, string drmUrl)
    {
        MessageEntities.Medium media = new() { Id = mediaId };
        PurchasedEntities.ListItem post = new()
        {
            Id = 20,
            PostedAt = new DateTime(2024, 1, 1),
            Author = new OF_DL.Models.Entities.Common.Author { Id = 99 },
            FromUser = null,
            Media = [media]
        };

        return new PurchasedEntities.PaidPostCollection
        {
            PaidPosts = new Dictionary<long, string> { { mediaId, drmUrl } },
            PaidPostMedia = [media],
            PaidPostObjects = [post]
        };
    }

    private static string NormalizeFolder(string folder) => folder.Replace("\\", "/");

    private sealed class DeterministicFileNameService : IFileNameService
    {
        public Task<string> BuildFilename(string fileFormat, Dictionary<string, string> values) =>
            Task.FromResult(fileFormat);

        public Task<Dictionary<string, string>> GetFilename(object info, object media, object author,
            List<string> selectedProperties, string username, Dictionary<string, long>? users = null) =>
            Task.FromResult(new Dictionary<string, string>());
    }
}
