// Used to generate tests in AesCipherTest.cs

// The script works by running "openssl enc [...]" (via WSL) to generate the
// expected encrypted values, and also verifies those values against the .NET
// BCL implementation as an extra validation before generating the tests.

Dictionary<string, (string, CipherMode?)> modes = new()
{
    ["ecb"] = ("iv: null, AesCipherMode.ECB", CipherMode.ECB),
    ["cbc"] = ("(byte[])iv.Clone(), AesCipherMode.CBC", CipherMode.CBC),
    ["cfb"] = ("(byte[])iv.Clone(), AesCipherMode.CFB", CipherMode.CFB),
    ["ctr"] = ("(byte[])iv.Clone(), AesCipherMode.CTR", null),
    ["ofb"] = ("(byte[])iv.Clone(), AesCipherMode.OFB", CipherMode.OFB),
};

Random random = new(123);

using IndentedTextWriter tw = new(Console.Out);

foreach ((string mode, (string modeCode, CipherMode? bclMode)) in modes)
{
    foreach (int keySize in new int[] { 128, 192, 256 })
    {
        foreach (int inputLength in new int[] { 16, 35, 64 })
        {
            foreach (bool pad in new bool[] { false, true })
            {
                // It is not allowed to use no padding on non-block lengths
                // It makes sense in cfb, ctr and ofb modes
                if (!pad && inputLength % 16 != 0 && mode is not ("cfb" or "ctr" or "ofb"))
                {
                    continue;
                }

                // It does not make sense to test padding for stream cipher modes
                // (and the OpenSSL, BCL implementations differ)
                if (pad && mode is "cfb" or "ctr" or "ofb")
                {
                    continue;
                }

                byte[] input = new byte[inputLength];
                random.NextBytes(input);

                byte[] key = new byte[keySize / 8];
                random.NextBytes(key);

                byte[] iv = new byte[16];
                random.NextBytes(iv);

                StringBuilder openSslCmd = new();

                openSslCmd.Append($"echo -n -e '{string.Join("", input.Select(b => $"\\x{b:x2}"))}' |");
                openSslCmd.Append($" openssl enc -e -aes-{keySize}-{mode}");
                openSslCmd.Append($" -K {Convert.ToHexString(key)}");
                if (mode != "ecb")
                {
                    openSslCmd.Append($" -iv {Convert.ToHexString(iv)}");
                }

                if (!pad)
                {
                    openSslCmd.Append(" -nopad");
                }

                ProcessStartInfo pi = new("wsl", openSslCmd.ToString())
                {
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                };

                byte[] expected;
                string error;

                using (MemoryStream ms = new())
                {
                    var p = Process.Start(pi);
                    p.StandardOutput.BaseStream.CopyTo(ms);
                    error = p.StandardError.ReadToEnd();

                    p.WaitForExit();

                    expected = ms.ToArray();
                }

                tw.WriteLine("[TestMethod]");
                tw.WriteLine($"public void AES_{mode.ToUpper()}_{keySize}_Length{inputLength}_{(pad ? "Pad" : "NoPad")}()");
                tw.WriteLine("{");
                tw.Indent++;

                WriteBytes(input);
                WriteBytes(key);
                if (mode != "ecb")
                {
                    WriteBytes(iv);
                }
                tw.WriteLine();

                if (!string.IsNullOrWhiteSpace(error))
                {
                    tw.WriteLine($"// {openSslCmd}");
                    tw.WriteLine($"Assert.Fail(@\"{error}\");");

                    tw.Indent--;
                    tw.WriteLine("}");
                    tw.WriteLine();
                    continue;
                }

                tw.WriteLine($"// {openSslCmd} | hd"); // pipe to hexdump
                WriteBytes(expected);
                tw.WriteLine();
                tw.WriteLine($"var actual = new AesCipher(key, {modeCode}, pkcs7Padding: {(pad ? "true" : "false")}).Encrypt(input);");
                tw.WriteLine();
                tw.WriteLine($"CollectionAssert.AreEqual(expected, actual);");

                if (bclMode is not null and not CipherMode.OFB and not CipherMode.CFB)
                {
                    // Verify the OpenSSL result is the same as the .NET BCL, just to be sure
                    Aes bcl = Aes.Create();
                    bcl.Key = key;
                    bcl.IV = iv;
                    bcl.Mode = bclMode.Value;
                    bcl.Padding = pad ? PaddingMode.PKCS7 : PaddingMode.None;
                    bcl.FeedbackSize = 128; // .NET is CFB8 by default, OpenSSL is CFB128
                    byte[] bclBytes = bcl.CreateEncryptor().TransformFinalBlock(input, 0, input.Length);

                    if (!bclBytes.AsSpan().SequenceEqual(expected))
                    {
                        tw.WriteLine();
                        tw.WriteLine(@"Assert.Inconclusive(@""OpenSSL does not match the .NET BCL");
                        tw.Indent++;
                        tw.WriteLine($@"OpenSSL: {Convert.ToHexString(expected)}");
                        tw.WriteLine($@"BCL:     {Convert.ToHexString(bclBytes)}"");");
                        tw.Indent--;
                    }
                }

                tw.WriteLine();
                tw.WriteLine($"var decrypted = new AesCipher(key, {modeCode}, pkcs7Padding: {(pad ? "true" : "false")}).Decrypt(actual);");
                tw.WriteLine();
                tw.WriteLine($"CollectionAssert.AreEqual(input, decrypted);");

                tw.Indent--;
                tw.WriteLine("}");
                tw.WriteLine();
            }
        }
    }
}

void WriteBytes(byte[] bytes, [CallerArgumentExpression(nameof(bytes))] string name = null)
{
    tw.WriteLine($"var {name} = new byte[]");
    tw.WriteLine("{");
    tw.Indent++;
    foreach (byte[] chunk in bytes.Chunk(16))
    {
        tw.WriteLine(string.Join(", ", chunk.Select(b => $"0x{b:x2}")) + ',');
    }
    tw.Indent--;
    tw.WriteLine("};");
}
