using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Security.Cryptography;
using Xunit;

namespace TeslaConsole.DiffTests
{
    /// <summary>
    /// Proves the source-built secure-config library (SecureConfig/Tesla.SecureConfig,
    /// emitting assembly TeslaSecureConfiguration) is behaviourally identical to the
    /// original vendored TeslaSecureConfiguration.dll for the parts that define the
    /// over-the-wire provisioning format: the OFB keystream cipher and the
    /// BasicConfigResponse byte layout. Byte-identity against the original binary
    /// transitively guarantees compatibility with the pod-side counterpart (the
    /// Launcher's hand-written OFBDuplexStream), which was built against that binary.
    ///
    /// The original is loaded out-of-band with Assembly.LoadFile (it shares identity
    /// with the source build); construction logic is identical reflection over both.
    /// </summary>
    public class SecureConfigCompatTests
    {
        private static readonly Assembly SourceCfg = typeof(Tesla.BasicConfigResponse).Assembly;
        private static readonly Assembly VendoredCfg = LoadVendored();

        private static Assembly LoadVendored()
        {
            string path = Path.Combine(AssemblyPaths.RepoRoot, "lib", "TeslaSecureConfiguration.dll");
            Assert.True(File.Exists(path), "Vendored baseline not found: " + path);
            return Assembly.LoadFile(path);
        }

        // Fixed, deterministic inputs so both assemblies see identical material.
        private static readonly byte[] Key = BuildBytes(32, i => (byte)(i * 7 + 1));
        private static readonly byte[] Iv = BuildBytes(16, i => (byte)(i * 13 + 5));
        private static readonly byte[] Plaintext =
            System.Text.Encoding.UTF8.GetBytes("The quick brown pod jumps over the lazy console. 0123456789!");

        [Fact]
        public void Baseline_And_Source_Are_Distinct_Assemblies_With_Same_Identity()
        {
            Assert.NotEqual(SourceCfg.Location, VendoredCfg.Location);
            Assert.Equal(VendoredCfg.GetName().Name, SourceCfg.GetName().Name);
            Assert.Equal(VendoredCfg.GetName().Version, SourceCfg.GetName().Version);
        }

        [Fact]
        public void OFBCryptoStream_Produces_Identical_Ciphertext()
        {
            byte[] fromVendored = Encrypt(VendoredCfg);
            byte[] fromSource = Encrypt(SourceCfg);
            Assert.Equal(fromVendored, fromSource);
            // Sanity: it actually encrypted (output differs from plaintext).
            Assert.NotEqual(Plaintext, fromSource);
        }

        [Fact]
        public void OFBCryptoStream_RoundTrips()
        {
            byte[] cipher = Encrypt(SourceCfg);
            byte[] plain = Decrypt(SourceCfg, cipher);
            Assert.Equal(Plaintext, plain);
        }

        [Fact]
        public void BasicConfigResponse_ToBytes_Is_Identical()
        {
            Assert.Equal(ConfigToBytes(VendoredCfg), ConfigToBytes(SourceCfg));
        }

        [Fact]
        public void BasicConfigResponse_RoundTrips_Across_Implementations()
        {
            // Bytes produced by the vendored impl must parse correctly in the source impl.
            byte[] bytes = ConfigToBytes(VendoredCfg);
            object parsed = Activator.CreateInstance(
                SourceCfg.GetType("Tesla.BasicConfigResponse", true), bytes);
            Assert.Equal("192.168.1.50", Prop(parsed, "Address").ToString());
            Assert.Equal("255.255.255.0", Prop(parsed, "Mask").ToString());
            Assert.Equal("192.168.1.1", Prop(parsed, "Gateway").ToString());
            Assert.Equal("8.8.8.8", Prop(parsed, "Dns").ToString());
            Assert.Equal("POD-01", Prop(parsed, "HostName"));
        }

        // ── helpers ──────────────────────────────────────────────────────────

        private static byte[] Encrypt(Assembly asm)
        {
            using var aes = Rijndael.Create();
            aes.Mode = CipherMode.ECB;
            aes.Key = Key;
            var ms = new MemoryStream();
            var ofb = NewOfb(asm, ms, aes.CreateEncryptor(), CryptoStreamMode.Write, Iv);
            ofb.Write(Plaintext, 0, Plaintext.Length);
            ofb.Flush();
            return ms.ToArray();
        }

        private static byte[] Decrypt(Assembly asm, byte[] cipher)
        {
            using var aes = Rijndael.Create();
            aes.Mode = CipherMode.ECB;
            aes.Key = Key;
            var ms = new MemoryStream(cipher);
            var ofb = NewOfb(asm, ms, aes.CreateEncryptor(), CryptoStreamMode.Read, Iv);
            var outBuf = new byte[Plaintext.Length];
            int off = 0;
            while (off < outBuf.Length)
            {
                int n = ofb.Read(outBuf, off, outBuf.Length - off);
                if (n == 0) break;
                off += n;
            }
            return outBuf;
        }

        private static Stream NewOfb(Assembly asm, Stream inner, ICryptoTransform t, CryptoStreamMode mode, byte[] iv)
            // OFBCryptoStream keeps the IV array by reference and reuses it as its
            // ping-pong keystream scratch buffer, so it MUST get its own copy — the
            // real callers always pass a freshly generated IV.
            => (Stream)Activator.CreateInstance(
                asm.GetType("System.Security.Cryptography.OFBCryptoStream", true),
                inner, t, mode, (byte[])iv.Clone());

        private static byte[] ConfigToBytes(Assembly asm)
        {
            Type t = asm.GetType("Tesla.BasicConfigResponse", true);
            object cfg = Activator.CreateInstance(t,
                IPAddress.Parse("192.168.1.50"),
                IPAddress.Parse("255.255.255.0"),
                IPAddress.Parse("192.168.1.1"),
                IPAddress.Parse("8.8.8.8"),
                "POD-01");
            return (byte[])t.GetMethod("ToBytes").Invoke(cfg, null);
        }

        private static object Prop(object o, string name) => o.GetType().GetProperty(name).GetValue(o, null);

        private static byte[] BuildBytes(int n, Func<int, byte> f)
        {
            var b = new byte[n];
            for (int i = 0; i < n; i++) b[i] = f(i);
            return b;
        }
    }
}
