using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OF_DL.Enumerations;
using OF_DL.Models;
using OF_DL.Models.Config;
using OF_DL.Models.Entities.Common;
using OF_DL.Models.OfdlApi;
using OF_DL.Services;
using UserEntities = OF_DL.Models.Entities.Users;

namespace OF_DL.Tests.Services;

public class ApiServiceTests
{
    [Fact]
    public void GetDynamicHeaders_ReturnsSignedHeaders()
    {
        FakeAuthService authService = new()
        {
            CurrentAuth = new Auth
            {
                UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_cookie=abc;"
            }
        };
        ApiService service = CreateService(authService);
        DynamicRules rules = new()
        {
            AppToken = "app-token",
            StaticParam = "static",
            Prefix = "prefix",
            Suffix = "suffix",
            ChecksumConstant = 7,
            ChecksumIndexes = [0, 5, 10, 15]
        };

        using DynamicRulesCacheScope _ = new(rules);

        Dictionary<string, string> headers = service.GetDynamicHeaders("/api2/v2/users", "?limit=1");

        Assert.Equal("application/json, text/plain", headers["accept"]);
        Assert.Equal("app-token", headers["app-token"]);
        Assert.Equal("auth_cookie=abc;", headers["cookie"]);
        Assert.Equal("unit-test-agent", headers["user-agent"]);
        Assert.Equal("xbc-token", headers["x-bc"]);
        Assert.Equal("123", headers["user-id"]);
        Assert.True(long.TryParse(headers["time"], out long timestamp));

        string expectedSign = BuildSign(rules, timestamp, "/api2/v2/users?limit=1", "123");
        Assert.Equal(expectedSign, headers["sign"]);
    }

    [Fact]
    public void GetDynamicHeaders_ThrowsWhenRulesInvalid()
    {
        FakeAuthService authService = new()
        {
            CurrentAuth = new Auth
            {
                UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_cookie=abc;"
            }
        };
        ApiService service = CreateService(authService);
        DynamicRules rules = new()
        {
            AppToken = null,
            StaticParam = "static",
            Prefix = null,
            Suffix = "suffix",
            ChecksumConstant = null,
            ChecksumIndexes = []
        };

        using DynamicRulesCacheScope _ = new(rules);

        Exception ex = Assert.Throws<Exception>(() => service.GetDynamicHeaders("/api2/v2/users", "?limit=1"));
        Assert.Contains("Invalid dynamic rules", ex.Message);
    }

    [Fact]
    public void GetDynamicHeaders_ThrowsWhenAuthMissingFields()
    {
        FakeAuthService authService = new()
        {
            CurrentAuth = new Auth
            {
                UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = null
            }
        };
        ApiService service = CreateService(authService);
        DynamicRules rules = new()
        {
            AppToken = "app-token",
            StaticParam = "static",
            Prefix = "prefix",
            Suffix = "suffix",
            ChecksumConstant = 1,
            ChecksumIndexes = [0]
        };

        using DynamicRulesCacheScope _ = new(rules);

        Exception ex = Assert.Throws<Exception>(() => service.GetDynamicHeaders("/api2/v2/users", "?limit=1"));
        Assert.Contains("Auth service is missing required fields", ex.Message);
    }

    [Fact]
    public async Task BuildHttpRequestMessage_BuildsUrlAndAddsHeaders()
    {
        FakeAuthService authService = new()
        {
            CurrentAuth = new Auth
            {
                UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_cookie=abc;"
            }
        };
        ApiService service = CreateService(authService);
        DynamicRules rules = new()
        {
            AppToken = "app-token",
            StaticParam = "static",
            Prefix = "prefix",
            Suffix = "suffix",
            ChecksumConstant = 7,
            ChecksumIndexes = [0]
        };

        using DynamicRulesCacheScope _ = new(rules);

        Dictionary<string, string> getParams = new() { { "limit", "10" }, { "offset", "5" } };
        HttpRequestMessage request = await InvokeBuildHttpRequestMessage(service, getParams, "/users");

        Assert.Equal("https://onlyfans.com/api2/v2/users?limit=10&offset=5", request.RequestUri?.ToString());
        Assert.True(request.Headers.Contains("app-token"));
        Assert.True(request.Headers.Contains("sign"));
        Assert.True(request.Headers.Contains("user-id"));
    }

    [Fact]
    public void DeserializeJson_ReturnsDefaultForWhitespace()
    {
        object? result = InvokeDeserializeJson(typeof(Dictionary<string, int>), " ", null);
        Assert.Null(result);
    }

    [Fact]
    public void DeserializeJson_ParsesValidJson()
    {
        object? result = InvokeDeserializeJson(typeof(Dictionary<string, int>), "{\"a\":1}", null);
        Assert.NotNull(result);
        Dictionary<string, int> dict = Assert.IsType<Dictionary<string, int>>(result);
        Assert.Equal(1, dict["a"]);
    }

    [Fact]
    public void UpdateGetParamsForDateSelection_BeforeAddsBeforePublishTime()
    {
        Dictionary<string, string> getParams = new();

        InvokeUpdateGetParamsForDateSelection(DownloadDateSelection.before, getParams, "123.000000");

        Assert.Equal("123.000000", getParams["beforePublishTime"]);
    }

    [Fact]
    public void UpdateGetParamsForDateSelection_AfterAddsAfterPublishTimeAndOrder()
    {
        Dictionary<string, string> getParams = new();

        InvokeUpdateGetParamsForDateSelection(DownloadDateSelection.after, getParams, "456.000000");

        Assert.Equal("publish_date_asc", getParams["order"]);
        Assert.Equal("456.000000", getParams["afterPublishTime"]);
    }

    [Theory]
    [InlineData("photo", "Images")]
    [InlineData("video", "Videos")]
    [InlineData("gif", "Videos")]
    [InlineData("audio", "Audios")]
    [InlineData("unknown", null)]
    public void ResolveMediaType_ReturnsExpectedValue(string input, string? expected)
    {
        string? result = InvokeResolveMediaType(input);

        Assert.Equal(expected, result);
    }

    [Fact]
    public void IsMediaTypeDownloadEnabled_RespectsConfigFlags()
    {
        Config config = new() { DownloadImages = false, DownloadVideos = false, DownloadAudios = false };
        ApiService service = new(new FakeAuthService(), new FakeConfigService(config), new MediaTrackingDbService());

        Assert.False(InvokeIsMediaTypeDownloadEnabled(service, "photo"));
        Assert.False(InvokeIsMediaTypeDownloadEnabled(service, "video"));
        Assert.False(InvokeIsMediaTypeDownloadEnabled(service, "audio"));
        Assert.True(InvokeIsMediaTypeDownloadEnabled(service, "other"));
    }

    [Fact]
    public void TryGetDrmInfo_ReturnsTrueWhenComplete()
    {
        Files files = new()
        {
            Drm = new Drm
            {
                Manifest = new Manifest { Dash = "dash" },
                Signature = new Signature
                {
                    Dash = new Dash
                    {
                        CloudFrontPolicy = "policy",
                        CloudFrontSignature = "signature",
                        CloudFrontKeyPairId = "kvp"
                    }
                }
            }
        };

        bool result = InvokeTryGetDrmInfo(files, out string manifestDash, out string policy,
            out string signature, out string kvp);

        Assert.True(result);
        Assert.Equal("dash", manifestDash);
        Assert.Equal("policy", policy);
        Assert.Equal("signature", signature);
        Assert.Equal("kvp", kvp);
    }

    [Fact]
    public void TryGetDrmInfo_ReturnsFalseWhenMissingFields()
    {
        Files files = new()
        {
            Drm = new Drm
            {
                Manifest = new Manifest { Dash = null },
                Signature = new Signature { Dash = new Dash { CloudFrontPolicy = "policy" } }
            }
        };

        bool result = InvokeTryGetDrmInfo(files, out _, out _, out _, out _);

        Assert.False(result);
    }

    [Fact]
    public void GetCurrentUserIdOrDefault_ReturnsMinValueWhenMissingOrInvalid()
    {
        ApiService serviceMissing =
            new(new FakeAuthService(), new FakeConfigService(new Config()), new MediaTrackingDbService());
        Assert.Equal(int.MinValue, InvokeGetCurrentUserIdOrDefault(serviceMissing));

        FakeAuthService authService = new() { CurrentAuth = new Auth { UserId = "not-a-number" } };
        ApiService serviceInvalid = new(authService, new FakeConfigService(new Config()), new MediaTrackingDbService());
        Assert.Equal(int.MinValue, InvokeGetCurrentUserIdOrDefault(serviceInvalid));
    }

    [Fact]
    public void GetCurrentUserIdOrDefault_ReturnsParsedUserId()
    {
        FakeAuthService authService = new() { CurrentAuth = new Auth { UserId = "42" } };
        ApiService service = new(authService, new FakeConfigService(new Config()), new MediaTrackingDbService());

        Assert.Equal(42, InvokeGetCurrentUserIdOrDefault(service));
    }

    [Fact]
    public void ConvertToUnixTimestampWithMicrosecondPrecision_ReturnsExpectedSeconds()
    {
        double epoch = InvokeConvertToUnixTimestampWithMicrosecondPrecision(
            new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc));
        double oneSecond = InvokeConvertToUnixTimestampWithMicrosecondPrecision(
            new DateTime(1970, 1, 1, 0, 0, 1, DateTimeKind.Utc));

        Assert.Equal(0, epoch, 6);
        Assert.Equal(1, oneSecond, 6);
    }

    [Fact]
    public async Task GetDrmMpdPssh_ReturnsSecondPssh()
    {
        string mpd = """
                     <?xml version="1.0" encoding="UTF-8"?>
                     <MPD xmlns:cenc="urn:mpeg:cenc:2013">
                       <Period>
                         <ContentProtection>
                           <cenc:pssh>FIRST</cenc:pssh>
                           <cenc:pssh>SECOND</cenc:pssh>
                         </ContentProtection>
                       </Period>
                     </MPD>
                     """;
        using SimpleHttpServer server = new(mpd);
        FakeAuthService authService = new()
        {
            CurrentAuth = new Auth
            {
                UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_id=1; sess=2;"
            }
        };
        ApiService service = CreateService(authService);

        string pssh = await service.GetDrmMpdPssh(server.Url.ToString(), "policy", "signature", "kvp");
        await server.Completion;

        Assert.Equal("SECOND", pssh);
    }

    [Fact]
    public async Task GetDrmMpdLastModified_ReturnsLastModifiedHeader()
    {
        DateTime lastModifiedUtc = new(2024, 2, 3, 4, 5, 6, DateTimeKind.Utc);
        using SimpleHttpServer server = new("<MPD />", lastModifiedUtc);
        FakeAuthService authService = new()
        {
            CurrentAuth = new Auth
            {
                UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_id=1; sess=2;"
            }
        };
        ApiService service = CreateService(authService);

        DateTime result =
            await service.GetDrmMpdLastModified(server.Url.ToString(), "policy", "signature", "kvp");
        await server.Completion;

        DateTime expectedLocal = lastModifiedUtc.ToLocalTime();
        Assert.True((result - expectedLocal).Duration() < TimeSpan.FromSeconds(1));
    }

    [Fact]
    public async Task GetUserInfo_ReturnsNullWhenAuthMissing()
    {
        ApiService service = CreateService(new FakeAuthService());
        using DynamicRulesCacheScope _ = new(BuildTestRules());

        UserEntities.User? user = await service.GetUserInfo("/users/me");

        Assert.Null(user);
    }

    [Fact]
    public async Task GetUserInfoById_ReturnsNullWhenAuthMissing()
    {
        ApiService service = CreateService(new FakeAuthService());
        using DynamicRulesCacheScope _ = new(BuildTestRules());

        JObject? user = await service.GetUserInfoById("/users/list?x[]=1");

        Assert.Null(user);
    }

    [Fact]
    public async Task GetActiveSubscriptions_ReturnsNullWhenAuthMissing()
    {
        ApiService service = CreateService(new FakeAuthService());
        using DynamicRulesCacheScope _ = new(BuildTestRules());

        Dictionary<string, long>? result = await service.GetActiveSubscriptions("/subscriptions", false);

        Assert.Null(result);
    }

    [Fact]
    public async Task GetExpiredSubscriptions_ReturnsNullWhenAuthMissing()
    {
        ApiService service = CreateService(new FakeAuthService());
        using DynamicRulesCacheScope _ = new(BuildTestRules());

        Dictionary<string, long>? result = await service.GetExpiredSubscriptions("/subscriptions", false);

        Assert.Null(result);
    }

    [Fact]
    public async Task GetLists_ReturnsNullWhenAuthMissing()
    {
        ApiService service = CreateService(new FakeAuthService());
        using DynamicRulesCacheScope _ = new(BuildTestRules());

        Dictionary<string, long>? result = await service.GetLists("/lists");

        Assert.Null(result);
    }

    [Fact]
    public async Task GetListUsers_ReturnsNullWhenAuthMissing()
    {
        ApiService service = CreateService(new FakeAuthService());
        using DynamicRulesCacheScope _ = new(BuildTestRules());

        List<string>? result = await service.GetListUsers("/lists/1/users");

        Assert.Null(result);
    }

    [Fact]
    public async Task GetMedia_ReturnsNullWhenAuthMissing()
    {
        ApiService service = CreateService(new FakeAuthService());
        using DynamicRulesCacheScope _ = new(BuildTestRules());

        Dictionary<long, string>? result = await service.GetMedia(MediaType.Stories, "/users/1/stories", null, "/tmp");

        Assert.Null(result);
    }

    private static ApiService CreateService(FakeAuthService authService) =>
        new(authService, new FakeConfigService(new Config()), new MediaTrackingDbService());

    private static DynamicRules BuildTestRules() =>
        new()
        {
            AppToken = "app-token",
            StaticParam = "static",
            Prefix = "prefix",
            Suffix = "suffix",
            ChecksumConstant = 7,
            ChecksumIndexes = [0]
        };

    private static async Task<HttpRequestMessage> InvokeBuildHttpRequestMessage(ApiService service,
        Dictionary<string, string> getParams, string endpoint)
    {
        MethodInfo method = typeof(ApiService).GetMethod("BuildHttpRequestMessage",
                                BindingFlags.NonPublic | BindingFlags.Instance)
                            ?? throw new InvalidOperationException("BuildHttpRequestMessage not found.");
        Task<HttpRequestMessage> task = (Task<HttpRequestMessage>)method.Invoke(service,
            [getParams, endpoint])!;
        return await task;
    }

    private static object? InvokeDeserializeJson(Type type, string? body, JsonSerializerSettings? settings)
    {
        MethodInfo method = typeof(ApiService).GetMethod("DeserializeJson",
                                BindingFlags.NonPublic | BindingFlags.Static)
                            ?? throw new InvalidOperationException("DeserializeJson not found.");
        MethodInfo generic = method.MakeGenericMethod(type);
        return generic.Invoke(null, [body, settings]);
    }

    private static void InvokeUpdateGetParamsForDateSelection(DownloadDateSelection selection,
        Dictionary<string, string> getParams, string? timestamp)
    {
        MethodInfo method = typeof(ApiService).GetMethod("UpdateGetParamsForDateSelection",
                                BindingFlags.NonPublic | BindingFlags.Static, null,
                                [
                                    typeof(DownloadDateSelection), typeof(Dictionary<string, string>).MakeByRefType(),
                                    typeof(string)
                                ],
                                null)
                            ?? throw new InvalidOperationException("UpdateGetParamsForDateSelection not found.");
        object?[] args = { selection, getParams, timestamp };
        method.Invoke(null, args);
    }

    private static string? InvokeResolveMediaType(string? type)
    {
        MethodInfo method = typeof(ApiService).GetMethod("ResolveMediaType",
                                BindingFlags.NonPublic | BindingFlags.Static)
                            ?? throw new InvalidOperationException("ResolveMediaType not found.");
        return (string?)method.Invoke(null, new object?[] { type });
    }

    private static bool InvokeIsMediaTypeDownloadEnabled(ApiService service, string? type)
    {
        MethodInfo method = typeof(ApiService).GetMethod("IsMediaTypeDownloadEnabled",
                                BindingFlags.NonPublic | BindingFlags.Instance)
                            ?? throw new InvalidOperationException("IsMediaTypeDownloadEnabled not found.");
        return (bool)method.Invoke(service, [type])!;
    }

    private static bool InvokeTryGetDrmInfo(Files files, out string manifestDash, out string cloudFrontPolicy,
        out string cloudFrontSignature, out string cloudFrontKeyPairId)
    {
        MethodInfo method = typeof(ApiService).GetMethod("TryGetDrmInfo",
                                BindingFlags.NonPublic | BindingFlags.Static)
                            ?? throw new InvalidOperationException("TryGetDrmInfo not found.");
        object?[] args = { files, null, null, null, null };
        bool result = (bool)method.Invoke(null, args)!;
        manifestDash = (string)args[1]!;
        cloudFrontPolicy = (string)args[2]!;
        cloudFrontSignature = (string)args[3]!;
        cloudFrontKeyPairId = (string)args[4]!;
        return result;
    }

    private static int InvokeGetCurrentUserIdOrDefault(ApiService service)
    {
        MethodInfo method = typeof(ApiService).GetMethod("GetCurrentUserIdOrDefault",
                                BindingFlags.NonPublic | BindingFlags.Instance)
                            ?? throw new InvalidOperationException("GetCurrentUserIdOrDefault not found.");
        return (int)method.Invoke(service, null)!;
    }

    private static double InvokeConvertToUnixTimestampWithMicrosecondPrecision(DateTime dateTime)
    {
        MethodInfo method = typeof(ApiService).GetMethod("ConvertToUnixTimestampWithMicrosecondPrecision",
                                BindingFlags.NonPublic | BindingFlags.Static)
                            ?? throw new InvalidOperationException(
                                "ConvertToUnixTimestampWithMicrosecondPrecision not found.");
        return (double)method.Invoke(null, [dateTime])!;
    }

    private static string BuildSign(DynamicRules rules, long timestamp, string pathWithQuery, string userId)
    {
        string input = $"{rules.StaticParam}\n{timestamp}\n{pathWithQuery}\n{userId}";
        byte[] hashBytes = SHA1.HashData(Encoding.UTF8.GetBytes(input));
        string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();

        Assert.NotNull(rules.ChecksumConstant);

        int checksum = rules.ChecksumIndexes.Aggregate(0, (current, index) => current + hashString[index]) +
                       rules.ChecksumConstant.Value;
        string checksumHex = checksum.ToString("X").ToLowerInvariant();

        return $"{rules.Prefix}:{hashString}:{checksumHex}:{rules.Suffix}";
    }
}
