using System;
using System.Security.Cryptography;
using System.Text;
using MajacloudSdk.Internal;
using NUnit.Framework;

namespace MajacloudSdk.Tests
{
    public class ConfigVerifierTests
    {
        private const string ProjectId = "11111111-2222-4333-8444-555555555555";
        private RSA _rsa;
        private string _publicKeyXml;

        [OneTimeSetUp]
        public void CreateKey()
        {
            _rsa = RSA.Create(2048);
            var p = _rsa.ExportParameters(false);
            _publicKeyXml = "<RSAKeyValue><Modulus>" + Convert.ToBase64String(p.Modulus) + "</Modulus><Exponent>" + Convert.ToBase64String(p.Exponent) + "</Exponent></RSAKeyValue>";
        }

        [OneTimeTearDown]
        public void DisposeKey() => _rsa?.Dispose();

        private string Envelope(long version, string projectId = ProjectId, bool sign = true, bool tamperChecksum = false)
        {
            var payload = "{\"project_id\":\"" + projectId + "\",\"version\":" + version + ",\"configs\":[{\"key\":\"enemy_damage\",\"type\":\"number\",\"value\":\"25\"},{\"key\":\"maintenance\",\"type\":\"boolean\",\"value\":\"true\"},{\"key\":\"rewards\",\"type\":\"json\",\"value\":\"{\\\"coin\\\":100}\"}]}";
            var data = Base64Url.Encode(Encoding.UTF8.GetBytes(payload));
            var checksum = MajacloudCrypto.Sha256Hex(data) + (tamperChecksum ? "0" : string.Empty);
            var signature = sign ? Convert.ToBase64String(_rsa.SignData(Encoding.UTF8.GetBytes(data), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)) : string.Empty;
            return JsonNode.Object().Set("ok", true).Set("project_id", ProjectId).Set("version", version).Set("checksum", checksum).Set("data", data).Set("signature", signature).Set("signature_algorithm", "rsa-sha256").ToJson();
        }

        [Test]
        public void VerifiesSignedEnvelope()
        {
            Assert.IsTrue(ConfigVerifier.TryParse(Envelope(12), ProjectId, _publicKeyXml, false, out var snapshot, out var error, out _), error);
            Assert.AreEqual(12, snapshot.Version);
            Assert.IsTrue(snapshot.SignatureVerified);
            Assert.AreEqual(3, snapshot.Keys.Count);
            Assert.AreEqual(25, snapshot.Values["enemy_damage"].AsNumber());
            Assert.IsTrue(snapshot.Values["maintenance"].AsBool());
            Assert.AreEqual(MajacloudConfigType.Json, snapshot.Values["rewards"].Type);
        }

        [Test]
        public void RejectsTamperedSignatureChecksumAndProject()
        {
            Assert.IsFalse(ConfigVerifier.TryParse(Envelope(1, tamperChecksum: true), ProjectId, _publicKeyXml, false, out _, out _, out var code));
            Assert.AreEqual(MajacloudErrorCode.SignatureInvalid, code);

            var other = RSA.Create(2048);
            var p = other.ExportParameters(false);
            var otherXml = "<RSAKeyValue><Modulus>" + Convert.ToBase64String(p.Modulus) + "</Modulus><Exponent>" + Convert.ToBase64String(p.Exponent) + "</Exponent></RSAKeyValue>";
            Assert.IsFalse(ConfigVerifier.TryParse(Envelope(1), ProjectId, otherXml, false, out _, out _, out code));
            Assert.AreEqual(MajacloudErrorCode.SignatureInvalid, code);

            Assert.IsFalse(ConfigVerifier.TryParse(Envelope(1, projectId: "other"), ProjectId, _publicKeyXml, false, out _, out var error, out _));
            StringAssert.Contains("project", error);
        }

        [Test]
        public void UnsignedOnlyWhenAllowed()
        {
            Assert.IsFalse(ConfigVerifier.TryParse(Envelope(1, sign: false), ProjectId, _publicKeyXml, false, out _, out _, out _));
            Assert.IsTrue(ConfigVerifier.TryParse(Envelope(1, sign: false), ProjectId, string.Empty, true, out var snapshot, out _, out _));
            Assert.IsFalse(snapshot.SignatureVerified);
        }

        [Test]
        public void CacheClearsBothKeysAndRespectsSize()
        {
            var storage = new MemoryStorage();
            var cache = new ConfigCache(storage, ProjectId, 64);
            Assert.IsFalse(cache.Save(new string('x', 100), "\"etag\""));
            Assert.IsTrue(cache.Save("{\"small\":1}", "\"etag\""));
            Assert.IsTrue(cache.TryLoad(out var json, out var etag));
            Assert.AreEqual("\"etag\"", etag);
            cache.Clear();
            Assert.IsFalse(cache.TryLoad(out json, out etag));
            Assert.IsNull(etag);
        }
    }

    public class SessionAndErrorTests
    {
        [Test]
        public void SessionStoreRoundTripsAndRejectsCorruption()
        {
            var storage = new MemoryStorage();
            var store = new SessionStore(storage, "project", "device");
            var data = SessionData.FromResponse(JsonNode.Parse("{\"access_token\":\"a.b\",\"expires_in\":3600,\"refresh_token\":\"sid.r\",\"refresh_expires_in\":100,\"player\":{\"id\":\"1\",\"player_id\":\"player_x\",\"display_name\":\"Nova\",\"auth_type\":\"guest\"}}"));
            store.Save(data);
            var loaded = store.Load();
            Assert.AreEqual("sid.r", loaded.RefreshToken);
            Assert.AreEqual("Nova", MajacloudPlayer.FromJson(JsonNode.Parse(loaded.PlayerJson)).DisplayName);
            Assert.IsFalse(loaded.AccessExpiresWithin(60));
            storage.SetString(StorageKeys.Session("project"), "v2.garbage!!");
            Assert.IsNull(store.Load());
        }

        [Test]
        public void ErrorMappingUsesBodyCodeThenStatus()
        {
            var response = new HttpResponse { Status = 409, Text = "{\"ok\":false,\"error\":{\"code\":\"revision_mismatch\",\"message\":\"Stale\",\"request_id\":\"r1\",\"details\":{\"current_revision\":4,\"next_claim_at\":\"2026-10-10T00:00:00Z\"}}}" };
            var error = MajacloudHttp.MapError(response);
            Assert.AreEqual(MajacloudErrorCode.RevisionMismatch, error.Code);
            Assert.AreEqual("r1", error.RequestId);
            Assert.AreEqual(4, error.GetDetailNumber("current_revision"));
            Assert.AreEqual(2026, error.GetDetailDateTime("next_claim_at").Value.Year);
            Assert.AreEqual(MajacloudErrorCode.RateLimited, MajacloudHttp.MapError(new HttpResponse { Status = 429, Text = "{}" }).Code);
            Assert.AreEqual(MajacloudErrorCode.Timeout, MajacloudHttp.MapError(new HttpResponse { IsTransportError = true, IsTimeout = true }).Code);
            Assert.IsTrue(MajacloudHttp.MapError(new HttpResponse { Status = 503, Text = "" }).IsRetryable);
        }

        [Test]
        public void RetryPolicyMatrix()
        {
            var get = new HttpRequest { Method = "GET", Idempotent = true };
            var post = new HttpRequest { Method = "POST", Idempotent = false };
            Assert.IsTrue(MajacloudHttp.ShouldRetry(get, new HttpResponse { IsTransportError = true }, 0, 2));
            Assert.IsFalse(MajacloudHttp.ShouldRetry(post, new HttpResponse { IsTransportError = true }, 0, 2));
            Assert.IsTrue(MajacloudHttp.ShouldRetry(post, new HttpResponse { Status = 429 }, 0, 2));
            Assert.IsFalse(MajacloudHttp.ShouldRetry(get, new HttpResponse { Status = 500 }, 2, 2));
            Assert.IsFalse(MajacloudHttp.ShouldRetry(get, new HttpResponse { Status = 404 }, 0, 2));
            Assert.AreEqual(5f, MajacloudHttp.BackoffSeconds(0, 5f));
            Assert.LessOrEqual(MajacloudHttp.BackoffSeconds(10, null), 8.3f);
        }

        [Test]
        public void LenientConfigGetters()
        {
            var number = new MajacloudConfigValue { Key = "n", Type = MajacloudConfigType.String, Raw = "42" };
            Assert.AreEqual(42, number.AsNumber());
            Assert.IsTrue(new MajacloudConfigValue { Type = MajacloudConfigType.Number, Raw = "1" }.AsBool());
            Assert.IsFalse(new MajacloudConfigValue { Type = MajacloudConfigType.String, Raw = "off" }.AsBool());
            Assert.IsNull(new MajacloudConfigValue { Type = MajacloudConfigType.String, Raw = "hello" }.AsNumber());
        }
    }
}
