#if SELFTEST
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NUnit.Framework;
using OpenCvSharp;

namespace TIASshot.Tests {

    /// <summary>
    /// CameraBase の撮影・保存（ShotOne / ShotMulti → RunShotLoop → SaveThread → SaveImages）の結合ロジックテスト（層②）．
    ///
    /// 仕様: SPEC_01「撮影」「撮影失敗時の挙動」「データ保存」
    ///   - 保存される画像: 元画像 `Shot{NO}.png`，sRGB 変換画像 `sRGB{CN}_{NO}.jpg`（指定した次元数ぶん），撮影情報 `Info.csv`
    ///   - 保存先: `[SaveFolder]/[日付]/[時刻]-[データ名]/`
    ///   - 画像の保存に失敗した場合は，保存処理の終了後にダイアログで通知する
    ///   - 撮影ボタンは保存が完了するまで無効のままとなり，保存処理の終了後に再び操作可能になる
    ///
    /// 失敗の注入: 撮影開始時（BeginShot．保存フォルダ作成後・Info.csv 作成前）に，保存先フォルダ内へ
    /// 保存予定の画像ファイルと同名のフォルダ（例 `Shot0001.png\`）を作る．Cv2.ImWrite はファイルを開けないと
    /// 例外を出さず false を返すため，IoUtil.SaveImage が false を返す状況になる（前提は
    /// <see cref="SaveImage_保存先と同名のフォルダがある_例外を出さずfalseを返す"/> で確認する）．
    /// 例外ではなく false で注入するのは，戻り値の無視（保存失敗が「全ての画像保存完了」と表示される）を検出するため．
    ///
    /// 観測は Form1.SelfTest.cs のアクセサ（SelfTestErrors・SelfTestMessages・SelfTestShotsEnabled）と保存ファイルで行う．
    /// 観測値は STA スレッド上で集めてテストスレッドへ返し，アサーションはテストスレッドで行う．
    /// 本クラスは SELFTEST 構成（Debug|x64）でのみコンパイルされる．
    /// </summary>
    [TestFixture]
    public class CameraBaseSaveIntegrationTests {

        // SaveThread が全枚数の保存に成功したときの表示（SPEC_01: 保存成功時のみ完了と表示する）
        private const string AllSavedMessage = "全ての画像保存完了";

        // 保存失敗の通知に含まれるべき語（ダイアログ＝ShowError で保存失敗と分かること）
        private const string SaveFailedPhrase = "保存に失敗";

        // ================================================================================
        // 前提: 失敗注入の手段そのものの確認（層①）
        // ================================================================================

        /// <summary>
        /// 保存先パスに同名のフォルダがある場合，IoUtil.SaveImage は例外を出さず false を返す．
        /// 以降の結合テストの失敗注入（同名フォルダ）が「false 返却」の経路に乗ることの前提確認．
        /// これが赤なら注入方法を見直すこと（例外になる場合は修正前のコードでも失敗経路に乗ってしまう）．
        /// </summary>
        [Test]
        public void SaveImage_保存先と同名のフォルダがある_例外を出さずfalseを返す() {
            // Arrange
            var dir = Path.Combine(Path.GetTempPath(), "TIASshotTests_" + Guid.NewGuid().ToString("N"));
            var path = Path.Combine(dir, "Shot0001.png");
            Directory.CreateDirectory(path);
            try {
                using (var img = new Mat(4, 4, MatType.CV_8UC3, Scalar.All(128))) {
                    var result = true;

                    // Act
                    Assert.DoesNotThrow((Action)(() => result = IoUtil.SaveImage(path, img)));

                    // Assert
                    Assert.That(result, Is.False, "同名フォルダがあり書き込めない場合は false で報告されること");
                }
            } finally {
                if (Directory.Exists(dir)) Directory.Delete(dir, true);
            }
        }

        // ================================================================================
        // 正常系
        // ================================================================================

        /// <summary>
        /// 単発撮影で保存に成功したとき，撮影画像・sRGB 変換画像（全次元）・Info.csv が保存先に作られ，
        /// 「全ての画像保存完了」が表示され，エラー通知が無く，撮影ボタンが再び有効になる．
        /// </summary>
        [Test, Category("Integration")]
        public void ShotOne_保存成功_全ファイルが保存され完了表示と撮影ボタン再有効化が行われる() {
            // Arrange / Act
            var obs = IntegrationHarness.Run(h => {
                var camera = h.CreateChartCamera();
                h.Calibrate(camera);
                return ShootAndObserve(h, camera, multi: false);
            });

            // Assert
            Assert.That(obs.Channels, Is.Not.Empty, "前提: 変換次元が 1 つ以上設定されていること");
            using (Assert.EnterMultipleScope()) {
                Assert.That(obs.SaveFolderUnderSaveRoot, Is.True, $"保存先が一時保存先の配下であること: {obs.SaveFolder}");
                Assert.That(obs.SavedFiles, Is.SupersetOf(obs.ExpectedFilesForNo1), "撮影画像・sRGB 変換画像・Info.csv が保存されること");
                Assert.That(obs.InfoHasShotLogHeader, Is.True, "Info.csv に撮影ログのヘッダ（Shot,Time(ms),Note）があること");
                Assert.That(obs.Messages, Has.Member(AllSavedMessage), "保存成功時は「全ての画像保存完了」が表示されること");
                Assert.That(obs.Errors, Is.Empty, "保存成功時はエラー通知が無いこと");
                Assert.That(obs.ShotsEnabled, Is.True, "保存完了後に撮影ボタンが再び有効になること");
            }
        }

        // ================================================================================
        // 失敗注入
        // ================================================================================

        /// <summary>
        /// 単発撮影で撮影画像の保存に失敗したとき（IoUtil.SaveImage が false），
        /// 保存失敗が通知され，「全ての画像保存完了」は表示されず，撮影ボタンは再び有効になる．
        /// </summary>
        [Test, Category("Integration")]
        public void ShotOne_撮影画像の保存失敗_保存失敗を通知し完了と表示せず撮影ボタンを再有効化する() {
            // Arrange / Act
            var obs = IntegrationHarness.Run(h => {
                var camera = h.Connect(h.Track(new SaveBlockingCamera(h.Form, h.ChartFrameFactory(), 1)));
                h.Calibrate(camera);
                return ShootAndObserve(h, camera, multi: false);
            });

            // Assert
            AssertSaveFailureReported(obs);
        }

        /// <summary>
        /// 連続撮影（3 枚）で 2 枚目の撮影画像の保存に失敗したとき，
        /// 保存失敗が通知され，「全ての画像保存完了」は表示されず，撮影ボタンは再び有効になる．
        /// </summary>
        [Test, Category("Integration")]
        public void ShotMulti_2枚目の保存失敗_保存失敗を通知し完了と表示せず撮影ボタンを再有効化する() {
            // Arrange / Act
            var obs = IntegrationHarness.Run(h => {
                var camera = h.Connect(h.Track(new SaveBlockingCamera(h.Form, h.ChartFrameFactory(), 2)));
                h.Calibrate(camera);
                h.Form.SelfTestSetMultiShots("3", "0");
                return ShootAndObserve(h, camera, multi: true);
            });

            // Assert
            AssertSaveFailureReported(obs);
        }

        // ================================================================================
        // 共通処理
        // ================================================================================

        private static void AssertSaveFailureReported(ShotObservation obs) {
            Assert.That(obs.InjectedPaths, Is.Not.Empty, "前提: 失敗注入（保存先と同名のフォルダ）が行われたこと");
            using (Assert.EnterMultipleScope()) {
                Assert.That(obs.Errors, Has.Some.Matches<string>(e => e != null && e.Contains(SaveFailedPhrase)),
                    $"保存失敗がエラー通知（ShowError）されること．通知: [{string.Join(" / ", obs.Errors)}]");
                Assert.That(obs.Messages, Has.No.Member(AllSavedMessage),
                    $"保存に失敗したのに「{AllSavedMessage}」と表示しないこと．表示履歴: [{string.Join(" / ", obs.Messages)}]");
                Assert.That(obs.ShotsEnabled, Is.True, "保存失敗後も撮影ボタンが再び有効になること（UI が固着しない）");
            }
        }

        /// <summary>
        /// 撮影ボタン押下（Form1.ExecuteShot）と同じく撮影ボタンを無効化してから撮影し，保存スレッドの終了を待って観測値を集める．
        /// 無効化しておかないと，再有効化されたかどうかを観測できない（校正完了で既に有効なため）．
        /// </summary>
        private static ShotObservation ShootAndObserve(IntegrationHarness h, SelfTestCamera camera, bool multi) {
            h.Form.SelfTestClearLog();
            h.Form.EnableShots(false);
            if (multi) {
                camera.ShotMulti();
            } else {
                camera.ShotOne();
            }
            h.WaitSaveFinished(camera);

            var folder = camera.CurrentSaveFolder;
            var obs = new ShotObservation {
                Errors = h.Form.SelfTestErrors.ToList(),
                Messages = h.Form.SelfTestMessages.ToList(),
                ShotsEnabled = h.Form.SelfTestShotsEnabled,
                SaveFolder = folder,
                Channels = GetConversionChannels(),
                InjectedPaths = (camera as SaveBlockingCamera)?.InjectedPaths.ToList() ?? new List<string>(),
            };
            // 期待ファイル名は一時設定が読み込まれている STA スレッド上で組み立てる（SPEC_01「撮影保存ファイル」）
            obs.ExpectedFilesForNo1.Add(Config.GetString("File/Info"));
            obs.ExpectedFilesForNo1.Add(ShotFileName("File/RgbImage", 0, 1));
            obs.ExpectedFilesForNo1.AddRange(obs.Channels.Select(ch => ShotFileName("File/SrgbImage", ch, 1)));
            if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder)) {
                obs.SaveFolderUnderSaveRoot = IsUnder(folder, h.SaveRoot);
                obs.SavedFiles = Directory.GetFiles(folder).Select(Path.GetFileName).ToList();
                var info = Path.Combine(folder, Config.GetString("File/Info"));
                obs.InfoHasShotLogHeader = File.Exists(info) && File.ReadAllText(info).Contains("Shot,Time(ms),Note");
            }
            return obs;
        }

        /// <summary>SPEC_01 のファイル名規則（{CN}: 変換次元 2 桁，{NO}: 連番 4 桁）で保存ファイル名を作る</summary>
        private static string ShotFileName(string key, int channel, int no) {
            return Config.GetString(key).Replace("{CN}", $"{channel:00}").Replace("{NO}", $"{no:0000}");
        }

        /// <summary>設定の変換次元（Calib/ConversionChannels．SPEC_02: カンマ区切り・4〜17 の範囲のみ有効）</summary>
        private static List<int> GetConversionChannels() {
            return Config.GetString("Calib/ConversionChannels")
                .Split(',')
                .Select(x => int.Parse(x.Trim()))
                .Where(x => x >= 4 && x <= 17)
                .Distinct()
                .OrderBy(x => x)
                .ToList();
        }

        private static bool IsUnder(string path, string root) {
            var p = Path.GetFullPath(path).TrimEnd('\\') + "\\";
            var r = Path.GetFullPath(root).TrimEnd('\\') + "\\";
            return p.StartsWith(r, StringComparison.OrdinalIgnoreCase);
        }

        /// <summary>STA スレッドで集めた観測値（テストスレッドで検証する）</summary>
        private sealed class ShotObservation {
            public List<string> Errors = new List<string>();
            public List<string> Messages = new List<string>();
            public bool ShotsEnabled;
            public string SaveFolder;
            public bool SaveFolderUnderSaveRoot;
            public List<string> SavedFiles = new List<string>();
            public bool InfoHasShotLogHeader;
            public List<int> Channels = new List<int>();
            public List<string> InjectedPaths = new List<string>();
            public List<string> ExpectedFilesForNo1 = new List<string>();
        }

        /// <summary>
        /// 指定した連番の撮影画像（Shot{NO}.png）を保存できなくする偽カメラ（失敗注入用）．
        /// 撮影開始時（保存フォルダ作成後）に，保存先フォルダへ同名のフォルダを作る．
        /// これにより Cv2.ImWrite が false を返し，IoUtil.SaveImage が false を返す（例外は出ない）．
        /// </summary>
        private sealed class SaveBlockingCamera : SelfTestCamera {

            private readonly int[] _blockedNos;

            public SaveBlockingCamera(Form1 form, Func<int, Mat> frameFactory, params int[] blockedNos) : base(form, frameFactory) {
                _blockedNos = blockedNos;
            }

            /// <summary>作成した同名フォルダのパス</summary>
            public List<string> InjectedPaths { get; } = new List<string>();

            protected override void BeginShot() {
                base.BeginShot();
                foreach (var no in _blockedNos) {
                    var path = Path.Combine(CurrentSaveFolder, ShotFileName("File/RgbImage", 0, no));
                    Directory.CreateDirectory(path);
                    InjectedPaths.Add(path);
                }
            }
        }
    }
}
#endif
