using System.Text.Json;
using PalladiumWallet.Core.Chain;
using PalladiumWallet.Core.Net;

namespace PalladiumWallet.Tests.Net;

public class PeerParsingTests
{
    [Fact]
    public void La_risposta_peers_subscribe_si_parsa_nel_formato_electrumx()
    {
        // Real format: [ip, hostname, ["v...", "pN", "tPORT", "sPORT"]].
        const string json = """
            [
              ["173.212.224.67", "173.212.224.67", ["v1.4.2", "p10000", "t50001", "s50002"]],
              ["10.0.0.1", "nodo.esempio.org", ["v1.4", "t"]],
              ["10.0.0.2", "solo-ssl.esempio.org", ["v1.4", "s50002"]],
              ["10.0.0.3", "senza-porte.esempio.org", ["v1.4"]]
            ]
            """;
        var peers = ElectrumApi.ParsePeers(JsonDocument.Parse(json).RootElement);

        Assert.Equal(3, peers.Count); // last entry has no ports → discarded

        Assert.Equal(new PeerInfo("173.212.224.67", 50001, 50002, "1.4.2"), peers[0]);
        // "t" without a number = default port (0 signals "resolve from profile").
        Assert.Equal(new PeerInfo("nodo.esempio.org", 0, null, "1.4"), peers[1]);
        Assert.Equal(new PeerInfo("solo-ssl.esempio.org", null, 50002, "1.4"), peers[2]);
    }

    [Theory]
    [InlineData("""{"not":"an array"}""")]
    [InlineData(""" "just a string" """)]
    [InlineData("42")]
    [InlineData("""[[1,2,3]]""")]                       // numeric ip/hostname
    [InlineData("""[["h","n",42]]""")]                  // features not an array
    [InlineData("""[["h","n",[42,{"x":1},null]]]""")]   // non-string features
    public void Una_risposta_peers_di_forma_inattesa_produce_una_lista_vuota(string json)
    {
        // The response is untrusted server data: any shape must degrade to an
        // empty list, never throw (found by fuzzing).
        Assert.Empty(ElectrumApi.ParsePeers(JsonDocument.Parse(json).RootElement));
    }

    [Fact]
    public void Una_stringa_json_con_utf8_invalido_viene_ignorata_senza_eccezioni()
    {
        // Raw 0x87 inside a JSON string: JsonDocument.Parse accepts the bytes but
        // GetString() cannot transcode them to UTF-16 (found by fuzzing). The
        // whole entry degrades to "no usable host/feature", not an exception.
        var bytes = "[[\"1.2.3.4\",\"#\",[\"v1\",\"t#\"]]]"u8.ToArray();
        var hostIdx = Array.IndexOf(bytes, (byte)'#');
        bytes[hostIdx] = 0x87;
        bytes[Array.LastIndexOf(bytes, (byte)'#')] = 0x87;

        using var doc = JsonDocument.Parse(bytes);
        Assert.Empty(ElectrumApi.ParsePeers(doc.RootElement));
    }
}

public class ServerRegistryTests
{
    private static string TempPath() =>
        Path.Combine(Path.GetTempPath(), $"plm-servers-{Guid.NewGuid()}.json");

    [Fact]
    public void Il_registro_parte_dai_bootstrap_del_profilo()
    {
        var path = TempPath();
        var registry = new ServerRegistry(ChainProfiles.Mainnet, path);

        Assert.Equal(ChainProfiles.Mainnet.BootstrapServers.Count, registry.All.Count);
        Assert.Equal("173.212.224.67", registry.Default!.Host);
        Assert.Equal(50001, registry.Default.PortFor(useSsl: false));
        Assert.Equal(50002, registry.Default.PortFor(useSsl: true));
    }

    [Fact]
    public void I_peer_scoperti_si_aggiungono_e_persistono_senza_duplicati()
    {
        var path = TempPath();
        try
        {
            var registry = new ServerRegistry(ChainProfiles.Mainnet, path);
            var bootstrapCount = registry.All.Count;

            // Direct merge via DiscoverAsync requires a client: test persistence
            // by simulating the discovered servers file.
            var discovered = new[] { new KnownServer("nuovo.esempio.org", 50001, 50002, "1.4.2") };
            File.WriteAllText(path, JsonSerializer.Serialize(discovered));

            var reloaded = new ServerRegistry(ChainProfiles.Mainnet, path);
            Assert.Equal(bootstrapCount + 1, reloaded.All.Count);
            Assert.Contains(reloaded.All, s => s.Host == "nuovo.esempio.org");

            // A bootstrap duplicate in the file must not double-count.
            File.WriteAllText(path, JsonSerializer.Serialize(new[]
            {
                new KnownServer("173.212.224.67", 50001, 50002),
                new KnownServer("nuovo.esempio.org", 50001, 50002),
            }));
            var deduped = new ServerRegistry(ChainProfiles.Mainnet, path);
            Assert.Equal(bootstrapCount + 1, deduped.All.Count);
        }
        finally
        {
            File.Delete(path);
        }
    }

    [Fact]
    public async Task La_discovery_aggiunge_i_peer_annunciati_e_li_persiste()
    {
        var path = TempPath();
        try
        {
            await using var server = new FakeElectrumServer();
            server.Handle("server.peers.subscribe", _ => new object[]
            {
                // Full announcement, port-less announcement (→ profile defaults),
                // and a bootstrap duplicate that must not be re-added.
                new object[] { "10.0.0.9", "nuovo.esempio.org", new[] { "v1.4.2", "t50001", "s50002" } },
                new object[] { "10.0.0.8", "senza-porte.esempio.org", new[] { "v1.4", "t", "s" } },
                new object[] { "173.212.224.67", "173.212.224.67", new[] { "v1.4", "t50001" } },
            });
            await using var client = await ElectrumClient.ConnectAsync(server.Host, server.Port, useSsl: false);

            var registry = new ServerRegistry(ChainProfiles.Mainnet, path);
            var bootstrapCount = registry.All.Count;

            var added = await registry.DiscoverAsync(client);

            Assert.Equal(2, added);
            Assert.Equal(bootstrapCount + 2, registry.All.Count);
            var defaulted = registry.All.Single(s => s.Host == "senza-porte.esempio.org");
            Assert.Equal(ChainProfiles.Mainnet.DefaultTcpPort, defaulted.TcpPort);
            Assert.Equal(ChainProfiles.Mainnet.DefaultSslPort, defaulted.SslPort);

            // Persisted for the next session (bootstrap servers excluded from the file).
            var reloaded = new ServerRegistry(ChainProfiles.Mainnet, path);
            Assert.Equal(bootstrapCount + 2, reloaded.All.Count);
            var savedJson = File.ReadAllText(path);
            Assert.DoesNotContain("173.212.224.67", savedJson);

            // A second discovery of the same peers adds nothing.
            Assert.Equal(0, await registry.DiscoverAsync(client));
        }
        finally
        {
            File.Delete(path);
        }
    }

    [Fact]
    public void Un_file_server_corrotto_non_blocca_l_avvio()
    {
        var path = TempPath();
        try
        {
            File.WriteAllText(path, "{ non-json ");
            var registry = new ServerRegistry(ChainProfiles.Mainnet, path);
            Assert.Equal(ChainProfiles.Mainnet.BootstrapServers.Count, registry.All.Count);
        }
        finally
        {
            File.Delete(path);
        }
    }
}
