using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using NUnit.Framework;
using OpenCvSharp;
using OpenCvSharp.Aruco;
using ArucoDictionary = OpenCvSharp.Aruco.Dictionary;

namespace TIASshot.Tests {
    /// <summary>
    /// ChartDetector のテスト（AR マーカー検出・上下判定・固定判定・マスク生成）
    ///
    /// fixture は実写画像ではなく，Dict4X4_50 の ID 40 / 41 をコードで描画した合成画像を使う．
    /// DetectChart は「画像入力 → マスク出力」の純粋処理で，外部依存は
    /// Config の Calib/ChartSetCriteria・Calib/ChartSetCount と showMessage コールバックだけであるため，
    /// 合成画像で検出 API の等価性（ID・コーナー順序・幾何）を検証できる．
    /// 実写のノイズ・照明ムラ・レンズ歪みは再現しないので，実機確認の代替ではない．
    ///
    /// Config は静的シングルトンのため [NonParallelizable] とし，各テストの SetUp で必ず読み直す．
    /// </summary>
    [TestFixture]
    [NonParallelizable]
    public class ChartDetectorTests {

        // 合成 fixture の寸法．ChartDetector が内部で使うモデル空間（810 x 1545）と同じにして
        // ホモグラフィがほぼ恒等変換になるようにする
        private const int ImageWidth = 810;
        private const int ImageHeight = 1545;

        // マーカーの描画サイズ．ChartDetector の PointsDst40 / PointsDst41 が示す 120px 四方に合わせる
        private const int MarkerSizePx = 120;

        // モデル空間上のマーカー位置（PointsDst40 / PointsDst41 の左上座標）
        private const int Marker40X = 345;
        private const int Marker40Y = 130;
        private const int Marker41X = 345;
        private const int Marker41Y = 1200;

        private const int MarkerId40 = 40;
        private const int MarkerId41 = 41;

        // テスト用 Config の値
        private const int ChartSetCount = 3;
        private const float ChartSetCriteria = 4.0f;

        // ChartSetCriteria を確実に超える移動量
        private const int MovementOverCriteria = 20;

        // ChartDetector が返すマスクの枚数（カラーチャートのパッチ数）
        private const int ExpectedMaskCount = 24;

        private const string UpsideDownMessage = "舌診チャートが上下逆方向です";

        private static readonly string TestConfigXml =
            "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
            "<Config>" +
            "  <Calib>" +
            "    <ChartSetCriteria>" + ChartSetCriteria.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) + "</ChartSetCriteria>" +
            "    <ChartSetCount>" + ChartSetCount + "</ChartSetCount>" +
            "  </Calib>" +
            "</Config>";

        private string _tempConfigPath;
        private List<string> _messages;

        [SetUp]
        public void SetUp() {
            var tempPath = Path.GetTempFileName();
            _tempConfigPath = tempPath + ".xml";
            File.Move(tempPath, _tempConfigPath);
            File.WriteAllText(_tempConfigPath, TestConfigXml, System.Text.Encoding.UTF8);
            Config.Load(_tempConfigPath);
            _messages = new List<string>();
        }

        [TearDown]
        public void TearDown() {
            if (_tempConfigPath != null && File.Exists(_tempConfigPath)) File.Delete(_tempConfigPath);
        }

        // ---------------------------------------------------------------
        // テスト本体
        // ---------------------------------------------------------------

        /// <summary>
        /// AR マーカーが 1 つも写っていない画像では検出が成立せず null を返す
        /// </summary>
        [Test]
        public void DetectChart_マーカーが無い画像_nullを返す() {
            var detector = new ChartDetector(RecordMessage);
            using (var img = CreateBlankImage()) {
                var masks = detector.DetectChart(img);
                DisposeMasks(masks);
                Assert.That(masks, Is.Null);
            }
        }

        /// <summary>
        /// ID 40 だけが写っている場合はマーカー座標が 8 点に満たないため null を返す
        /// </summary>
        [Test]
        public void DetectChart_ID40のみの画像_nullを返す() {
            var detector = new ChartDetector(RecordMessage);
            using (var img = CreateBlankImage()) {
                DrawMarker(img, MarkerId40, Marker40X, Marker40Y);
                var masks = detector.DetectChart(img);
                DisposeMasks(masks);
                Assert.That(masks, Is.Null);
            }
        }

        /// <summary>
        /// ID 40 が ID 41 より下にある（チャートが上下逆）場合は null を返す
        /// </summary>
        [Test]
        public void DetectChart_チャートが上下逆_nullを返す() {
            var detector = new ChartDetector(RecordMessage);
            using (var img = CreateUpsideDownChartImage()) {
                var masks = detector.DetectChart(img);
                DisposeMasks(masks);
                Assert.That(masks, Is.Null);
            }
        }

        /// <summary>
        /// ID 40 が ID 41 より下にある場合は上下逆である旨が呼び出し側に通知される
        /// </summary>
        [Test]
        public void DetectChart_チャートが上下逆_上下逆を通知する() {
            var detector = new ChartDetector(RecordMessage);
            using (var img = CreateUpsideDownChartImage()) {
                DisposeMasks(detector.DetectChart(img));
                Assert.That(_messages, Does.Contain(UpsideDownMessage));
            }
        }

        /// <summary>
        /// 固定判定が成立するまで（Calib/ChartSetCount フレーム分）は null を返す
        ///
        /// 1 フレーム目は _lastPosition が原点のため移動量が閾値を超えてカウントが 0 に落ちる．
        /// したがってマスクが返るのは ChartSetCount + 1 フレーム目となる．
        /// </summary>
        [Test]
        public void DetectChart_固定判定が成立するまで_nullを返す() {
            var detector = new ChartDetector(RecordMessage);
            using (var img = CreateChartImage(Marker40X, Marker40Y, Marker41X, Marker41Y)) {
                for (int frame = 1; frame <= ChartSetCount; frame++) {
                    var masks = detector.DetectChart(img);
                    DisposeMasks(masks);
                    Assert.That(masks, Is.Null, $"{frame} フレーム目ではまだ固定判定は成立しないはず");
                }
            }
        }

        /// <summary>
        /// マーカーが安定して検出され続けると，固定判定の成立フレームで 24 枚のマスクが返る
        /// </summary>
        [Test]
        public void DetectChart_マーカーが安定検出_24枚のマスクを返す() {
            var detector = new ChartDetector(RecordMessage);
            using (var img = CreateChartImage(Marker40X, Marker40Y, Marker41X, Marker41Y)) {
                var masks = DetectUntilStable(detector, img);
                try {
                    Assert.That(masks, Has.Count.EqualTo(ExpectedMaskCount));
                } finally {
                    DisposeMasks(masks);
                }
            }
        }

        /// <summary>
        /// 返るマスクはすべて入力画像と同じサイズである（入力画像座標系のマスクとして使える）
        /// </summary>
        [Test]
        public void DetectChart_返るマスク_入力画像と同じサイズ() {
            var detector = new ChartDetector(RecordMessage);
            using (var img = CreateChartImage(Marker40X, Marker40Y, Marker41X, Marker41Y)) {
                var masks = DetectUntilStable(detector, img);
                try {
                    Assert.That(masks, Is.All.Matches<Mat>(
                        m => m.Width == ImageWidth && m.Height == ImageHeight));
                } finally {
                    DisposeMasks(masks);
                }
            }
        }

        /// <summary>
        /// マーカーが Calib/ChartSetCriteria を超えて動くと固定判定カウントがリセットされる
        ///
        /// 安定フレームを ChartSetCount 回与えた直後にマーカーを動かすと，
        /// リセットが無ければそのフレームでマスクが返るが，リセットされるため null になる．
        /// </summary>
        [Test]
        public void DetectChart_基準を超えて動く_固定判定がリセットされる() {
            var detector = new ChartDetector(RecordMessage);
            using (var stillImg = CreateChartImage(Marker40X, Marker40Y, Marker41X, Marker41Y))
            using (var movedImg = CreateChartImage(
                       Marker40X + MovementOverCriteria, Marker40Y,
                       Marker41X + MovementOverCriteria, Marker41Y)) {
                for (int frame = 1; frame <= ChartSetCount; frame++) {
                    DisposeMasks(detector.DetectChart(stillImg));
                }
                var masks = detector.DetectChart(movedImg);
                DisposeMasks(masks);
                Assert.That(masks, Is.Null);
            }
        }

        /// <summary>
        /// 固定判定成立後の検出（ホモグラフィ計算・ワープ）で生成する中間 Mat を呼び出し内で解放し，
        /// ネイティブメモリを GC ファイナライザ任せで滞留させないこと
        ///
        /// GC を止めた状態（NoGCRegion）でマスク生成まで進む検出を繰り返し，プライベートメモリの増分を見る．
        /// 中間 Mat（とくに 810 x 1545 x 3 のワープ画像 約 3.75MB）が未解放だと呼び出し回数に比例して増える．
        /// 返されたマスクはテスト側で解放する．Windows ヒープは大きなブロックを解放時に OS へ返すため，
        /// 解放されていれば増分はほぼ 0 になる．
        /// </summary>
        [Test]
        [Category("Memory")]
        public void DetectChart_固定判定成立後の検出を繰り返す_中間Matのネイティブメモリを滞留させない() {
            const int Iterations = 10;
            const long LeakThresholdBytes = 20L * 1024 * 1024; // 未解放なら約 37MB 増える
            var detector = new ChartDetector(RecordMessage);
            using (var img = CreateChartImage(Marker40X, Marker40Y, Marker41X, Marker41Y)) {
                // Arrange: 固定判定を成立させ，ネイティブ側の初回確保（検出器の内部バッファ等）も済ませる
                DisposeMasks(DetectUntilStable(detector, img));
                for (int i = 0; i < 3; i++) DisposeMasks(detector.DetectChart(img));
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();

                Assert.That(GC.TryStartNoGCRegion(64L * 1024 * 1024), Is.True, "NoGCRegion を開始できること（前提）");
                long before, after;
                try {
                    before = GetPrivateBytes();
                    // Act
                    for (int i = 0; i < Iterations; i++) {
                        var masks = detector.DetectChart(img);
                        Assert.That(masks, Has.Count.EqualTo(ExpectedMaskCount), "毎回マスク生成まで進むこと（前提）");
                        DisposeMasks(masks);
                    }
                    after = GetPrivateBytes();
                } finally {
                    if (System.Runtime.GCSettings.LatencyMode == System.Runtime.GCLatencyMode.NoGCRegion) GC.EndNoGCRegion();
                }

                // Assert
                Assert.That(after - before, Is.LessThan(LeakThresholdBytes),
                    $"{Iterations} 回の検出でプライベートメモリが {(after - before) / 1024 / 1024}MB 増えた（中間 Mat の未解放）");
            }
        }

        // ---------------------------------------------------------------
        // ヘルパー
        // ---------------------------------------------------------------

        private static long GetPrivateBytes() {
            using (var process = Process.GetCurrentProcess()) {
                return process.PrivateMemorySize64;
            }
        }

        private void RecordMessage(string message) {
            _messages.Add(message);
        }

        /// <summary>
        /// 固定判定が成立するまで同じ画像を与え，成立フレームの結果を返す
        /// </summary>
        private static List<Mat> DetectUntilStable(ChartDetector detector, Mat img) {
            List<Mat> masks = null;
            for (int frame = 0; frame <= ChartSetCount; frame++) {
                DisposeMasks(masks);
                masks = detector.DetectChart(img);
            }
            return masks;
        }

        private static void DisposeMasks(List<Mat> masks) {
            if (masks == null) return;
            foreach (var mask in masks) mask.Dispose();
        }

        /// <summary>
        /// マーカーを含まない白い BGR 画像を作る
        /// </summary>
        private static Mat CreateBlankImage() {
            return new Mat(ImageHeight, ImageWidth, MatType.CV_8UC3, Scalar.All(255));
        }

        /// <summary>
        /// ID 40（上）と ID 41（下）を指定位置に描いた合成チャート画像を作る
        /// </summary>
        private static Mat CreateChartImage(int x40, int y40, int x41, int y41) {
            var img = CreateBlankImage();
            try {
                DrawMarker(img, MarkerId40, x40, y40);
                DrawMarker(img, MarkerId41, x41, y41);
                return img;
            } catch {
                img.Dispose();
                throw;
            }
        }

        /// <summary>
        /// ID 40 と ID 41 の上下を入れ替えた（チャートが上下逆の）合成画像を作る
        /// </summary>
        private static Mat CreateUpsideDownChartImage() {
            return CreateChartImage(Marker40X, Marker41Y, Marker41X, Marker40Y);
        }

        /// <summary>
        /// Dict4X4_50 のマーカーを画像上の指定位置に描画する
        /// </summary>
        private static void DrawMarker(Mat canvas, int markerId, int x, int y) {
            using (ArucoDictionary dict = CvAruco.GetPredefinedDictionary(PredefinedDictionaryType.Dict4X4_50))
            using (var marker = new Mat())
            using (var markerBgr = new Mat())
            using (var roi = new Mat(canvas, new Rect(x, y, MarkerSizePx, MarkerSizePx))) {
                // borderBits = 1（Dict4X4_50 の既定）．sidePixels には黒枠を含んだ全体サイズを指定する
                dict.GenerateImageMarker(markerId, MarkerSizePx, marker, 1);
                Cv2.CvtColor(marker, markerBgr, ColorConversionCodes.GRAY2BGR);
                markerBgr.CopyTo(roi);
            }
        }
    }
}
