using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using NUnit.Framework;
using OpenCvSharp;

namespace TIASshot.Tests {
    /// <summary>
    /// ImageStats のテスト（ピュアロジック層）
    ///
    /// GUIDE_08: ハードウェア非依存の計算ロジックを NUnit で検証する。
    /// ShotWhiteBoard・TryDisableColorEnhancement・Form1 ボタンはハードウェア結合層につき対象外。
    ///
    /// 飽和定義: 輝度（BGR2GRAY: 0.114*B + 0.587*G + 0.299*R）が 254 以上の画素を飽和とカウントする。
    /// TECH_02 Q8 では「チャンネル別 max」も記録対象とするが、飽和率の算出は輝度ベース（現実装の契約）。
    /// </summary>
    [TestFixture]
    public class ImageStatsTests {

        // ---------------------------------------------------------------
        // ImageStats.Calc — ch別 mean / max
        // ---------------------------------------------------------------

        /// <summary>
        /// Calc_全画素同一値_chMeanとchMaxが正しく返る:
        /// 全画素 B=100, G=100, R=100 → MeanB/G/R=100, MaxB/G/R=100
        /// </summary>
        [Test]
        public void Calc_全画素同一値_chMeanとchMaxが正しく返る() {
            // Arrange: 2x2 の全画素が B=100, G=100, R=100
            using (var mat = new Mat(2, 2, MatType.CV_8UC3, new Scalar(100, 100, 100))) {
                // Act
                var stats = ImageStats.Calc(mat);

                // Assert: ch別 mean / max
                Assert.AreEqual(100.0, stats.MeanB, 1e-3, "MeanB = 100");
                Assert.AreEqual(100.0, stats.MeanG, 1e-3, "MeanG = 100");
                Assert.AreEqual(100.0, stats.MeanR, 1e-3, "MeanR = 100");
                Assert.AreEqual(100.0, stats.MaxB,  1e-3, "MaxB = 100");
                Assert.AreEqual(100.0, stats.MaxG,  1e-3, "MaxG = 100");
                Assert.AreEqual(100.0, stats.MaxR,  1e-3, "MaxR = 100");
            }
        }

        /// <summary>
        /// Calc_ch別に異なる値_各chのMeanとMaxが分離される:
        /// 全画素 B=50, G=120, R=200 → MeanB=50, MeanG=120, MeanR=200（ch間の混在なし）
        /// </summary>
        [Test]
        public void Calc_ch別に異なる値_各chのMeanとMaxが分離される() {
            // Arrange: 3x3 の全画素が B=50, G=120, R=200（BGR 順）
            using (var mat = new Mat(3, 3, MatType.CV_8UC3, new Scalar(50, 120, 200))) {
                // Act
                var stats = ImageStats.Calc(mat);

                // Assert
                Assert.AreEqual(50.0,  stats.MeanB, 1e-3, "MeanB = 50");
                Assert.AreEqual(120.0, stats.MeanG, 1e-3, "MeanG = 120");
                Assert.AreEqual(200.0, stats.MeanR, 1e-3, "MeanR = 200");
                Assert.AreEqual(50.0,  stats.MaxB,  1e-3, "MaxB = 50");
                Assert.AreEqual(120.0, stats.MaxG,  1e-3, "MaxG = 120");
                Assert.AreEqual(200.0, stats.MaxR,  1e-3, "MaxR = 200");
            }
        }

        /// <summary>
        /// Calc_2行の異なる画素_chMaxが最大値を返す:
        /// 行0: B=10, G=20, R=30、行1: B=50, G=60, R=70 → MaxB=50, MaxG=60, MaxR=70
        /// MeanB=(10+50)/2=30（画素数=各行1画素の1x1ではなく2行1列の計2画素の平均）
        /// </summary>
        [Test]
        public void Calc_2行異なる画素_chMaxが最大値を返す() {
            // Arrange: 2行1列（2画素）、行ごとに値が異なる
            using (var mat = new Mat(2, 1, MatType.CV_8UC3)) {
                mat.At<Vec3b>(0, 0) = new Vec3b(10, 20, 30);   // B=10, G=20, R=30
                mat.At<Vec3b>(1, 0) = new Vec3b(50, 60, 70);   // B=50, G=60, R=70

                // Act
                var stats = ImageStats.Calc(mat);

                // Assert: MaxB=50, MaxG=60, MaxR=70（2画素の最大値）
                Assert.AreEqual(50.0, stats.MaxB, 1e-3, "MaxB は 2 画素の最大値 50");
                Assert.AreEqual(60.0, stats.MaxG, 1e-3, "MaxG は 2 画素の最大値 60");
                Assert.AreEqual(70.0, stats.MaxR, 1e-3, "MaxR は 2 画素の最大値 70");

                // Assert: MeanB=(10+50)/2=30, MeanG=(20+60)/2=40, MeanR=(30+70)/2=50
                Assert.AreEqual(30.0, stats.MeanB, 1e-3, "MeanB = (10+50)/2 = 30");
                Assert.AreEqual(40.0, stats.MeanG, 1e-3, "MeanG = (20+60)/2 = 40");
                Assert.AreEqual(50.0, stats.MeanR, 1e-3, "MeanR = (30+70)/2 = 50");
            }
        }

        // ---------------------------------------------------------------
        // ImageStats.Calc — 飽和率（輝度ベース・BGR2GRAY）
        // ---------------------------------------------------------------

        /// <summary>
        /// Calc_全画素同一値100_飽和率0:
        /// 全画素 (B,G,R)=(100,100,100) → 輝度=100 → 飽和なし → SaturatedRatio=0.0
        /// </summary>
        [Test]
        public void Calc_全画素同一値100_飽和率0() {
            using (var mat = new Mat(2, 2, MatType.CV_8UC3, new Scalar(100, 100, 100))) {
                var stats = ImageStats.Calc(mat);
                Assert.AreEqual(0.0, stats.SaturatedRatio, 1e-6, "全輝度 100 → 飽和率 0");
            }
        }

        /// <summary>
        /// Calc_全画素255_飽和率1_0:
        /// 全画素 (B,G,R)=(255,255,255) → 輝度=255 → 全画素飽和 → SaturatedRatio=1.0
        /// </summary>
        [Test]
        public void Calc_全画素255_飽和率1_0() {
            using (var mat = new Mat(2, 2, MatType.CV_8UC3, new Scalar(255, 255, 255))) {
                var stats = ImageStats.Calc(mat);
                Assert.AreEqual(1.0, stats.SaturatedRatio, 1e-6, "全画素 255 → 飽和率 1.0");
            }
        }

        /// <summary>
        /// Calc_全画素255_p99は255:
        /// 全画素 255 → p99=255 であること
        /// </summary>
        [Test]
        public void Calc_全画素255_p99は255() {
            using (var mat = new Mat(2, 2, MatType.CV_8UC3, new Scalar(255, 255, 255))) {
                var stats = ImageStats.Calc(mat);
                Assert.AreEqual(255.0, stats.P99, 1e-3, "全画素 255 → p99 = 255");
            }
        }

        /// <summary>
        /// Calc_全画素同一値100_p99は100:
        /// 全画素 (B,G,R)=(100,100,100) → 輝度=100 → p99=100
        /// </summary>
        [Test]
        public void Calc_全画素同一値100_p99は100() {
            using (var mat = new Mat(2, 2, MatType.CV_8UC3, new Scalar(100, 100, 100))) {
                var stats = ImageStats.Calc(mat);
                Assert.AreEqual(100.0, stats.P99, 1e-3, "全輝度 100 → p99 = 100");
            }
        }

        /// <summary>
        /// Calc_飽和画素割合の境界_SaturatedRatioが正しい割合:
        /// 4画素のうち1画素だけ輝度 >= 254（全ch=255）、残り3画素は輝度 100
        /// → SaturatedRatio = 1/4 = 0.25
        ///
        /// 飽和の定義: 輝度（BGR2GRAY: 0.114*B + 0.587*G + 0.299*R）が ≥ 254 のとき飽和。
        /// ch別に255であっても輝度 < 254 なら非飽和（例: B=255, G=0, R=0 → 輝度 ≒ 29）。
        /// </summary>
        [Test]
        public void Calc_飽和画素1つ4画素中_SaturatedRatioが0_25() {
            // Arrange: 2x2（4画素）、(0,0)のみ全ch=255（確実に輝度 >= 254）、残りは 100
            using (var mat = new Mat(2, 2, MatType.CV_8UC3, new Scalar(100, 100, 100))) {
                mat.At<Vec3b>(0, 0) = new Vec3b(255, 255, 255);  // 輝度 = 255 >= 254 → 飽和

                // Act
                var stats = ImageStats.Calc(mat);

                // Assert: 4画素中1画素飽和 → 1/4 = 0.25
                Assert.AreEqual(0.25, stats.SaturatedRatio, 1e-6, "4画素中1画素飽和 → SaturatedRatio = 0.25");
            }
        }

        /// <summary>
        /// Calc_R=255のみ高輝度_輝度ベースで飽和判定されないこと:
        /// B=0, G=0, R=255 → BGR2GRAY ≒ 0.299*255 ≒ 76.2 → 輝度 < 254 → 飽和なし
        ///
        /// これは「チャンネル別に255でも輝度ベースでは非飽和」という定義の明示的な固定。
        /// </summary>
        [Test]
        public void Calc_R255のみ_輝度ベースでは飽和しない() {
            // Arrange: B=0, G=0, R=255（輝度 ≒ 76.2 → 254 未満）
            using (var mat = new Mat(1, 1, MatType.CV_8UC3, new Scalar(0, 0, 255))) {
                var stats = ImageStats.Calc(mat);

                // Assert: 輝度ベースでは飽和なし
                Assert.AreEqual(0.0, stats.SaturatedRatio, 1e-6,
                    "R=255のみ → BGR2GRAY ≒ 76 < 254 → 輝度ベース飽和率 = 0");
            }
        }

        /// <summary>
        /// Calc_G=255のみ_輝度ベースで飽和しない:
        /// B=0, G=255, R=0 → BGR2GRAY ≒ 0.587*255 ≒ 149.7 → 輝度 < 254 → 飽和なし
        /// </summary>
        [Test]
        public void Calc_G255のみ_輝度ベースでは飽和しない() {
            using (var mat = new Mat(1, 1, MatType.CV_8UC3, new Scalar(0, 255, 0))) {
                var stats = ImageStats.Calc(mat);
                Assert.AreEqual(0.0, stats.SaturatedRatio, 1e-6,
                    "G=255のみ → BGR2GRAY ≒ 150 < 254 → 飽和率 = 0");
            }
        }

        // ---------------------------------------------------------------
        // ImageStats.CalcPercentile — 通常系
        // ---------------------------------------------------------------

        /// <summary>
        /// CalcPercentile_全画素同一値150_p50は150:
        /// 全画素 = 150 → p50 = 150
        /// </summary>
        [Test]
        public void CalcPercentile_全画素同一値150_p50は150() {
            using (var gray = new Mat(4, 4, MatType.CV_8U, new Scalar(150))) {
                double p50 = ImageStats.CalcPercentile(gray, 50.0);
                Assert.AreEqual(150.0, p50, 1e-3, "全画素 150 → p50 = 150");
            }
        }

        /// <summary>
        /// CalcPercentile_全画素同一値200_p99は200:
        /// 全画素 = 200 → p99 = 200
        /// </summary>
        [Test]
        public void CalcPercentile_全画素同一値200_p99は200() {
            using (var gray = new Mat(4, 4, MatType.CV_8U, new Scalar(200))) {
                double p99 = ImageStats.CalcPercentile(gray, 99.0);
                Assert.AreEqual(200.0, p99, 1e-3, "全画素 200 → p99 = 200");
            }
        }

        /// <summary>
        /// CalcPercentile_全画素同一値100_p100は100:
        /// 全画素 = 100 → p100 = 100
        /// </summary>
        [Test]
        public void CalcPercentile_全画素同一値100_p100は100() {
            using (var gray = new Mat(4, 4, MatType.CV_8U, new Scalar(100))) {
                double p100 = ImageStats.CalcPercentile(gray, 100.0);
                Assert.AreEqual(100.0, p100, 1e-3, "全画素 100 → p100 = 100");
            }
        }

        /// <summary>
        /// CalcPercentile_2種の輝度値_p50境界:
        /// 2画素: 値=100 と 値=200 → p50 は threshold=2*0.5=1.0
        /// bin[100]=1.0 で cumulative=1.0 >= 1.0 → p50=100
        /// </summary>
        [Test]
        public void CalcPercentile_2画素100と200_p50は100() {
            // Arrange: 1行2列（2画素）
            using (var gray = new Mat(1, 2, MatType.CV_8U)) {
                gray.At<byte>(0, 0) = 100;
                gray.At<byte>(0, 1) = 200;

                // Act
                double p50 = ImageStats.CalcPercentile(gray, 50.0);

                // Assert: threshold = 2 * 0.5 = 1.0。bin[100]=1.0 → cumulative=1.0 >= 1.0 → 100
                Assert.AreEqual(100.0, p50, 1e-3, "p50: bin[100]=1 → cumulative=1 >= threshold=1 → p50=100");
            }
        }

        /// <summary>
        /// CalcPercentile_2画素100と200_p99は200:
        /// threshold = 2 * 0.99 = 1.98。bin[100]=1.0 → cumulative=1.0 < 1.98。
        /// bin[200]=1.0 → cumulative=2.0 >= 1.98 → p99=200
        /// </summary>
        [Test]
        public void CalcPercentile_2画素100と200_p99は200() {
            using (var gray = new Mat(1, 2, MatType.CV_8U)) {
                gray.At<byte>(0, 0) = 100;
                gray.At<byte>(0, 1) = 200;

                double p99 = ImageStats.CalcPercentile(gray, 99.0);
                Assert.AreEqual(200.0, p99, 1e-3, "p99=200（上位1%に 200 が含まれるため）");
            }
        }

        // ---------------------------------------------------------------
        // ImageStats.CalcPercentile — 空 Mat
        // ---------------------------------------------------------------

        /// <summary>
        /// CalcPercentile_空Mat_0を返す:
        /// 0行0列（画素数 = 0）→ total = 0 → 0.0 を返す
        /// </summary>
        [Test]
        public void CalcPercentile_空Mat_0を返す() {
            // Arrange: 0行0列の CV_8U
            using (var gray = new Mat(0, 0, MatType.CV_8U)) {
                double result = ImageStats.CalcPercentile(gray, 99.0);
                Assert.AreEqual(0.0, result, 1e-6, "空 Mat → 0.0 を返す");
            }
        }

        // ---------------------------------------------------------------
        // ImageStats.BuildJson — 構造・キー・値の検証
        // ---------------------------------------------------------------

        /// <summary>
        /// BuildJson_metaのみstatsなし_有効なJSONが生成される:
        /// meta に string / int / bool / double を混在させ、JSON として parse できること
        /// </summary>
        [Test]
        public void BuildJson_metaのみ_有効なJSON文字列が生成される() {
            // Arrange
            var meta = new Dictionary<string, object> {
                { "capture_datetime", "2026-06-07 12:00:00" },
                { "num_images", 5 },
                { "color_enhancement_off", true },
                { "exposure_us", 12345.6 },
            };

            // Act
            string json = ImageStats.BuildJson(meta, null);

            // Assert: null / 空でない
            Assert.IsNotNull(json, "json が null であってはならない");
            Assert.IsNotEmpty(json, "json が空文字列であってはならない");

            // Assert: '{' / '}' で囲まれている（最低限の JSON 外枠）
            Assert.IsTrue(json.TrimStart().StartsWith("{"), "JSON は '{' で始まる");
            Assert.IsTrue(json.TrimEnd().EndsWith("}"),  "JSON は '}' で終わる");
        }

        /// <summary>
        /// BuildJson_metaの各型_キーと値が正しくシリアライズされる:
        /// bool → true/false、int → 数値、double → G6/InvariantCulture 書式、string → クォート
        /// </summary>
        [Test]
        public void BuildJson_bool値_truefalseがシリアライズされる() {
            var meta = new Dictionary<string, object> {
                { "flag_on",  true  },
                { "flag_off", false },
            };

            string json = ImageStats.BuildJson(meta, null);

            // bool は JSON の true / false リテラルになること
            StringAssert.Contains("\"flag_on\": true",  json, "true が literal true になること");
            StringAssert.Contains("\"flag_off\": false", json, "false が literal false になること");
        }

        /// <summary>
        /// BuildJson_int値_数値リテラルがシリアライズされる
        /// </summary>
        [Test]
        public void BuildJson_int値_数値リテラルがシリアライズされる() {
            var meta = new Dictionary<string, object> {
                { "num_images", 10 },
            };

            string json = ImageStats.BuildJson(meta, null);

            // int は引用符なしの数値リテラル
            StringAssert.Contains("\"num_images\": 10", json, "int は数値リテラルになること");
        }

        /// <summary>
        /// BuildJson_double値_G6InvariantCultureで書式化される:
        /// 1.23456789 → G6 書式（6桁有効数字）= "1.23457"（InvariantCulture: 小数点は '.'）
        /// </summary>
        [Test]
        public void BuildJson_double値_G6InvariantCultureで書式化される() {
            var meta = new Dictionary<string, object> {
                { "val", 1.23456789 },
            };

            string json = ImageStats.BuildJson(meta, null);

            // G6 + InvariantCulture: 1.23456789 → "1.23457"（四捨五入6桁）
            StringAssert.Contains("\"val\": 1.23457", json, "double は G6 InvariantCulture で書式化される");
            // ロケール依存のカンマ区切り（例: "1,23457"）が混入していないこと
            Assert.IsFalse(json.Contains("1,23457"), "小数点はカンマではなくピリオドを使う（InvariantCulture）");
        }

        /// <summary>
        /// BuildJson_string値_ダブルクォートでエスケープされる:
        /// 値中の '"' は '\"' にエスケープされること
        /// </summary>
        [Test]
        public void BuildJson_string値中のダブルクォート_エスケープされる() {
            var meta = new Dictionary<string, object> {
                { "desc", "say \"hello\"" },
            };

            string json = ImageStats.BuildJson(meta, null);

            // '"' が '\\"' にエスケープされた文字列として含まれること
            StringAssert.Contains("say \\\"hello\\\"", json, "\" は \\\" にエスケープされること");
        }

        /// <summary>
        /// BuildJson_string値中のバックスラッシュ_エスケープされる:
        /// 値中の '\' は '\\' にエスケープされること
        /// </summary>
        [Test]
        public void BuildJson_string値中のバックスラッシュ_エスケープされる() {
            var meta = new Dictionary<string, object> {
                { "path", @"C:\TIAS_Data" },
            };

            string json = ImageStats.BuildJson(meta, null);

            // '\' が '\\' にエスケープされた文字列として含まれること
            StringAssert.Contains(@"C:\\TIAS_Data", json, @"\ は \\ にエスケープされること");
        }

        /// <summary>
        /// BuildJson_statsリスト_frame_statsキーと各フレームのフィールドが生成される:
        /// 1フレーム分の FrameStats を与えたとき、"frame_stats" 配列が生成され
        /// "frame", "saturated_ratio", "p99_luminance", "mean_B/G/R", "max_B/G/R" が含まれること
        /// </summary>
        [Test]
        public void BuildJson_1フレームのstats_frame_statsキーと全フィールドが生成される() {
            // Arrange
            var meta = new Dictionary<string, object> { { "device", "DFK23UX249" } };
            var stats = new List<FrameStats> {
                new FrameStats {
                    SaturatedRatio = 0.0,
                    P99            = 181.0,
                    MeanB          = 120.5,
                    MeanG          = 130.0,
                    MeanR          = 140.0,
                    MaxB           = 200.0,
                    MaxG           = 210.0,
                    MaxR           = 220.0,
                }
            };

            // Act
            string json = ImageStats.BuildJson(meta, stats);

            // Assert: "frame_stats" 配列が存在する
            StringAssert.Contains("\"frame_stats\"", json, "frame_stats キーが存在すること");

            // Assert: フレーム番号・各フィールドが含まれる
            StringAssert.Contains("\"frame\": 1",            json, "frame 番号 1 が含まれること");
            StringAssert.Contains("\"saturated_ratio\"",     json, "saturated_ratio キーが含まれること");
            StringAssert.Contains("\"p99_luminance\"",       json, "p99_luminance キーが含まれること");
            StringAssert.Contains("\"mean_B\"",              json, "mean_B キーが含まれること");
            StringAssert.Contains("\"mean_G\"",              json, "mean_G キーが含まれること");
            StringAssert.Contains("\"mean_R\"",              json, "mean_R キーが含まれること");
            StringAssert.Contains("\"max_B\"",               json, "max_B キーが含まれること");
            StringAssert.Contains("\"max_G\"",               json, "max_G キーが含まれること");
            StringAssert.Contains("\"max_R\"",               json, "max_R キーが含まれること");
        }

        /// <summary>
        /// BuildJson_2フレームのstats_frame番号が1から始まり連番になる
        /// </summary>
        [Test]
        public void BuildJson_2フレームのstats_frame番号が1から連番() {
            var meta = new Dictionary<string, object> { { "n", 2 } };
            var stats = new List<FrameStats> {
                new FrameStats { SaturatedRatio = 0.0, P99 = 100.0 },
                new FrameStats { SaturatedRatio = 0.0, P99 = 110.0 },
            };

            string json = ImageStats.BuildJson(meta, stats);

            StringAssert.Contains("\"frame\": 1", json, "1枚目のフレーム番号は 1");
            StringAssert.Contains("\"frame\": 2", json, "2枚目のフレーム番号は 2");
        }

        /// <summary>
        /// BuildJson_statsがnull_frame_statsキーが生成されない
        /// </summary>
        [Test]
        public void BuildJson_statsがnull_frame_statsキーが生成されない() {
            var meta = new Dictionary<string, object> { { "key", "value" } };

            string json = ImageStats.BuildJson(meta, null);

            Assert.IsFalse(json.Contains("frame_stats"),
                "stats が null のとき frame_stats キーは生成されない");
        }

        /// <summary>
        /// BuildJson_statsが空リスト_frame_statsキーが生成されない
        /// </summary>
        [Test]
        public void BuildJson_statsが空リスト_frame_statsキーが生成されない() {
            var meta = new Dictionary<string, object> { { "key", "value" } };

            string json = ImageStats.BuildJson(meta, new List<FrameStats>());

            Assert.IsFalse(json.Contains("frame_stats"),
                "stats が空リストのとき frame_stats キーは生成されない");
        }

        /// <summary>
        /// BuildJson_SaturatedRatioが0_saturated_ratioが0にシリアライズされる:
        /// SaturatedRatio = 0.0 → "saturated_ratio": 0
        /// </summary>
        [Test]
        public void BuildJson_SaturatedRatioが0_JSON内で0が出力される() {
            var meta = new Dictionary<string, object>();
            var stats = new List<FrameStats> {
                new FrameStats { SaturatedRatio = 0.0, P99 = 181.0 }
            };

            string json = ImageStats.BuildJson(meta, stats);

            // G6 で 0.0 → "0"
            StringAssert.Contains("\"saturated_ratio\": 0", json,
                "飽和率 0 は JSON に 0 としてシリアライズされること");
        }

        // ---------------------------------------------------------------
        // 異常系 — Calc の引数検証
        // ---------------------------------------------------------------

        /// <summary>
        /// Calc_nullを渡す_ArgumentNullExceptionをスローする
        /// </summary>
        [Test]
        public void Calc_null_ArgumentNullExceptionをスローする() {
            Assert.Throws<ArgumentNullException>(
                () => ImageStats.Calc(null),
                "null Mat → ArgumentNullException");
        }

        /// <summary>
        /// Calc_CV_8UC1のMatを渡す_ArgumentExceptionをスローする:
        /// CV_8UC3 以外の Mat は受け付けない
        /// </summary>
        [Test]
        public void Calc_CV8UC1Mat_ArgumentExceptionをスローする() {
            using (var gray = new Mat(2, 2, MatType.CV_8U, new Scalar(100))) {
                Assert.Throws<ArgumentException>(
                    () => ImageStats.Calc(gray),
                    "CV_8UC1 → ArgumentException（CV_8UC3 以外は不可）");
            }
        }

        /// <summary>
        /// Calc_CV_64FC3のMatを渡す_ArgumentExceptionをスローする:
        /// 64 bit 浮動小数点型のカラー Mat も拒否される
        /// </summary>
        [Test]
        public void Calc_CV64FC3Mat_ArgumentExceptionをスローする() {
            using (var mat = new Mat(2, 2, MatType.CV_64FC3)) {
                Assert.Throws<ArgumentException>(
                    () => ImageStats.Calc(mat),
                    "CV_64FC3 → ArgumentException（CV_8UC3 以外は不可）");
            }
        }

        // ---------------------------------------------------------------
        // 異常系 — CalcPercentile の引数検証
        // ---------------------------------------------------------------

        /// <summary>
        /// CalcPercentile_nullを渡す_ArgumentNullExceptionをスローする
        /// </summary>
        [Test]
        public void CalcPercentile_null_ArgumentNullExceptionをスローする() {
            Assert.Throws<ArgumentNullException>(
                () => ImageStats.CalcPercentile(null, 99.0),
                "null Mat → ArgumentNullException");
        }

        /// <summary>
        /// CalcPercentile_CV_8UC3のMatを渡す_ArgumentExceptionをスローする:
        /// CalcPercentile は CV_8U（グレースケール）専用
        /// </summary>
        [Test]
        public void CalcPercentile_CV8UC3Mat_ArgumentExceptionをスローする() {
            using (var bgr = new Mat(2, 2, MatType.CV_8UC3, new Scalar(100, 100, 100))) {
                Assert.Throws<ArgumentException>(
                    () => ImageStats.CalcPercentile(bgr, 99.0),
                    "CV_8UC3 → ArgumentException（CV_8U 以外は不可）");
            }
        }
    }
}
