using System;
using System.Diagnostics;
using NUnit.Framework;
using OpenCvSharp;

namespace TIASshot.Tests {
    /// <summary>
    /// PLAN_02 優先度2（ConvertImage の中間 Mat 未 Dispose）修正の効果検証ベンチマーク．
    ///
    /// 修正前（リーク版）と修正後（using 版）の ConvertImage を同条件で繰り返し呼び出し，
    /// プロセスのメモリ使用量（PrivateMemorySize64 / WorkingSet64）を比較する．
    ///
    /// 注意:
    /// - OpenCvSharp の Mat はネイティブメモリを持つ．未 Dispose の Mat は GC+ファイナライザが
    ///   走るまで解放されない．連続撮影の本番フローでは GC 前に確保が積み上がり OOM クラッシュする．
    ///   本ベンチはループ中に GC を呼ばないことでこの機序を再現する．
    /// - 通常のテスト実行では走らない（[Explicit] + Category("Benchmark")）．
    ///   実行は vstest の TestCaseFilter で明示指定する（README 末尾の実行コマンド参照）．
    ///
    /// 環境変数でスケール調整可:
    ///   BENCH_WIDTH      画像幅          (既定 2440)
    ///   BENCH_HEIGHT     画像高          (既定 1840)
    ///   BENCH_CHANNEL    拡張チャネル数  (既定 17)
    ///   BENCH_FRAMES     繰り返し回数    (既定 40)
    ///   BENCH_CEILING_GB 打ち切り上限GB  (既定 10)  ← リーク版がここに達したら「限界」として停止
    /// </summary>
    [TestFixture]
    [Explicit("メモリ計測ベンチマーク．フルスケールで大量のメモリを確保するため明示実行のみ．")]
    [Category("Benchmark")]
    public class ConvertImageMemoryBenchmark {

        static int EnvInt(string name, int fallback) {
            var v = Environment.GetEnvironmentVariable(name);
            return int.TryParse(v, out var n) ? n : fallback;
        }

        static double EnvDouble(string name, double fallback) {
            var v = Environment.GetEnvironmentVariable(name);
            return double.TryParse(v, out var n) ? n : fallback;
        }

        /// <summary>現在のプロセスのメモリ使用量をサンプリングする．</summary>
        static (long priv, long ws) Sample() {
            using (var p = Process.GetCurrentProcess()) {
                p.Refresh();
                return (p.PrivateMemorySize64, p.WorkingSet64);
            }
        }

        static double GB(long bytes) => bytes / (1024.0 * 1024.0 * 1024.0);

        /// <summary>未参照 Mat のネイティブメモリを GC+ファイナライザで回収する．</summary>
        static void ReclaimNative() {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }

        /// <summary>
        /// 修正前（リーク版）の ConvertImage を忠実に再現したもの．
        /// 中間 Mat（flatten / extended / converted / convertedImage）を Dispose しない．
        /// （git 0003cc2^ の ColorCorrector.ConvertImage(Mat,Mat) と等価）
        /// </summary>
        static Mat ConvertImageLeaky(ColorCorrector corrector, Mat src, Mat conv) {
            if (src.Type() != MatType.CV_64FC3) {
                src.ConvertTo(src, MatType.CV_64FC3);
            }
            var flatten = src.Reshape(3, src.Height * src.Width);
            var extended = corrector.ExtendMat(flatten, conv.Rows);
            var converted = (extended * conv).ToMat();
            var convertedImage = converted.Reshape(3, src.Height);

            var convImg8 = new Mat();
            convertedImage.ConvertTo(convImg8, MatType.CV_8UC3);
            return convImg8; // flatten / extended / converted / convertedImage は放置（＝リーク）
        }

        [Test]
        public void Compare_LeakyVsFixed_MemoryUsage() {
            int width = EnvInt("BENCH_WIDTH", 2440);
            int height = EnvInt("BENCH_HEIGHT", 1840);
            int channel = EnvInt("BENCH_CHANNEL", 17);
            int frames = EnvInt("BENCH_FRAMES", 40);
            long ceiling = (long)(EnvDouble("BENCH_CEILING_GB", 10.0) * 1024 * 1024 * 1024);

            var corrector = new ColorCorrector(null, null, new int[] { channel });

            // 変換行列（channel x 3）．値は任意（メモリ挙動に影響しない）．
            using (var conv = new Mat(channel, 3, MatType.CV_64FC1, new Scalar(0.01))) {

                TestContext.WriteLine("=== ConvertImage メモリ計測ベンチマーク（PLAN_02 優先度2） ===");
                TestContext.WriteLine($"画像: {width}x{height} CV_8UC3 / チャネル: {channel} / 繰り返し: {frames} 回");
                TestContext.WriteLine($"打ち切り上限: {GB(ceiling):F1} GB (PrivateMemory)");
                TestContext.WriteLine("");

                // --- 修正後（using 版）を先にクリーンな状態で計測 ---
                var fixedResult = RunVariant(
                    "修正後（using 版）",
                    (src) => corrector.ConvertImage(src, conv),
                    width, height, frames, ceiling);

                // リーク版の前にネイティブメモリを回収して baseline を揃える
                ReclaimNative();

                // --- 修正前（リーク版）を計測 ---
                var leakyResult = RunVariant(
                    "修正前（リーク版）",
                    (src) => ConvertImageLeaky(corrector, src, conv),
                    width, height, frames, ceiling);

                // 放置したネイティブメモリを後始末（後続テストへの影響を防ぐ）
                ReclaimNative();

                // --- 比較サマリ ---
                TestContext.WriteLine("");
                TestContext.WriteLine("================ 比較サマリ ================");
                PrintRow("項目", "修正前（リーク版）", "修正後（using 版）");
                PrintRow("完了/打ち切り",
                    leakyResult.Aborted ? $"打ち切り(iter {leakyResult.Iterations})" : $"完走({leakyResult.Iterations})",
                    fixedResult.Aborted ? $"打ち切り(iter {fixedResult.Iterations})" : $"完走({fixedResult.Iterations})");
                PrintRow("Private baseline", $"{GB(leakyResult.BasePriv):F2} GB", $"{GB(fixedResult.BasePriv):F2} GB");
                PrintRow("Private peak", $"{GB(leakyResult.PeakPriv):F2} GB", $"{GB(fixedResult.PeakPriv):F2} GB");
                PrintRow("Private 増分(peak-base)", $"{GB(leakyResult.PeakPriv - leakyResult.BasePriv):F2} GB", $"{GB(fixedResult.PeakPriv - fixedResult.BasePriv):F2} GB");
                PrintRow("WorkingSet peak", $"{GB(leakyResult.PeakWs):F2} GB", $"{GB(fixedResult.PeakWs):F2} GB");
                PrintRow("1回あたり平均増分",
                    $"{(leakyResult.Iterations > 0 ? GB(leakyResult.PeakPriv - leakyResult.BasePriv) / leakyResult.Iterations * 1024 : 0):F0} MB",
                    $"{(fixedResult.Iterations > 0 ? GB(fixedResult.PeakPriv - fixedResult.BasePriv) / fixedResult.Iterations * 1024 : 0):F0} MB");
                TestContext.WriteLine("============================================");

                var leakyGrowth = leakyResult.PeakPriv - leakyResult.BasePriv;
                var fixedGrowth = fixedResult.PeakPriv - fixedResult.BasePriv;
                TestContext.WriteLine("");
                TestContext.WriteLine(
                    $"判定: 修正後の増分({GB(fixedGrowth):F2} GB) は 修正前の増分({GB(leakyGrowth):F2} GB) の "
                    + $"{(leakyGrowth > 0 ? (double)fixedGrowth / leakyGrowth * 100 : 0):F1}% に抑制された．");

                // この閾値判定はあくまで「修正が効いていること」の目安（緩め）．
                Assert.That(fixedGrowth, Is.LessThan(leakyGrowth),
                    "修正後の方がメモリ増分が小さいこと（修正が効いている）");
            }
        }

        class VariantResult {
            public long BasePriv, PeakPriv, FinalPriv, PeakWs;
            public int Iterations;
            public bool Aborted;
        }

        /// <summary>
        /// 1 つの ConvertImage バリアントを frames 回繰り返し，メモリ推移を計測する．
        /// src は in-place 書き換え（CV_64FC3）副作用があるため毎回新規生成して using で破棄する．
        /// 戻り値（convImg8）は呼び出し側相当で破棄する（中間 Mat のリークだけを切り出すため）．
        /// </summary>
        VariantResult RunVariant(string label, Func<Mat, Mat> convert, int width, int height, int frames, long ceiling) {
            ReclaimNative();
            var (basePriv, baseWs) = Sample();
            var r = new VariantResult { BasePriv = basePriv, PeakPriv = basePriv, PeakWs = baseWs };

            TestContext.WriteLine($"--- {label} ---");
            TestContext.WriteLine($"baseline: Private {GB(basePriv):F2} GB / WS {GB(baseWs):F2} GB");

            int i = 0;
            for (; i < frames; i++) {
                using (var src = new Mat(height, width, MatType.CV_8UC3, new Scalar(50, 100, 150)))
                using (var dst = convert(src)) {
                    // dst は using で破棄（呼び出し側 SaveImages の using 相当）
                }

                var (priv, ws) = Sample();
                if (priv > r.PeakPriv) r.PeakPriv = priv;
                if (ws > r.PeakWs) r.PeakWs = ws;

                if ((i + 1) % 5 == 0 || i == 0) {
                    TestContext.WriteLine($"  iter {i + 1,3}: Private {GB(priv):F2} GB / WS {GB(ws):F2} GB");
                }

                if (priv >= ceiling) {
                    TestContext.WriteLine($"  ★ 上限 {GB(ceiling):F1} GB に到達．iter {i + 1} で打ち切り（リーク蓄積の限界）．");
                    r.Aborted = true;
                    i++;
                    break;
                }
            }

            var (finalPriv, _) = Sample();
            r.FinalPriv = finalPriv;
            r.Iterations = i;
            TestContext.WriteLine($"結果: iter {i} / peak Private {GB(r.PeakPriv):F2} GB / peak WS {GB(r.PeakWs):F2} GB / 増分 {GB(r.PeakPriv - r.BasePriv):F2} GB");
            TestContext.WriteLine("");
            return r;
        }

        static void PrintRow(string a, string b, string c) {
            TestContext.WriteLine($"{a,-24} | {b,-22} | {c,-22}");
        }
    }
}
