using System;
using NUnit.Framework;

namespace TIASshot.Tests {

    /// <summary>
    /// 撮影・保存の信頼性（PLAN_05「束 B の方針」）で CameraBase から切り出した判断関数の単体テスト（層①）．
    ///
    /// 仕様: PLAN_05「束 B の方針」（2026-10-06 ユーザー合意）
    ///   - 校正中の例外は「校正に失敗しました」と（理由つきで）通知し，同じ失敗を繰り返し通知しない
    ///   - 次に校正が完了するまで，チャート検出の表示を「校正に失敗しました（理由）．チャートの検出からやり直しています」に差し替える．
    ///     ただしチャートが上下逆の案内（設置し直しの案内）は差し替えない（ユーザー合意済み）
    ///   - 校正画像の保存に失敗しても校正済みとし，「自動校正完了（校正画像の保存に失敗）」のように警告する
    ///   - 保存キューの待ち時間は「30 秒」と「撮影間隔の 2 倍」の大きい方
    /// </summary>
    [TestFixture]
    public class CameraBaseReliabilityDecisionTests {

        // 校正失敗後に検出中の表示を差し替える文の末尾（PLAN_05「束 B の方針」）
        private const string RetryingSuffix = "．チャートの検出からやり直しています";

        // ================================================================================
        // FormatCalibrationFailure（校正失敗の通知文）
        // ================================================================================

        /// <summary>校正失敗の通知は「校正に失敗しました」で始まり，理由（例外メッセージ）を含む</summary>
        [Test]
        public void FormatCalibrationFailure_例外_校正に失敗しましたで始まり理由を含む() {
            var text = CameraBase.FormatCalibrationFailure(new InvalidOperationException("理由X"));

            using (Assert.EnterMultipleScope()) {
                Assert.That(text, Does.StartWith("校正に失敗しました"));
                Assert.That(text, Does.Contain("理由X"));
            }
        }

        /// <summary>
        /// 失敗の通知文は，検出中の表示を差し替える文（…チャートの検出からやり直しています）とは区別できる
        /// （通知は 1 回，差し替えは毎フレームのため，同じ文だと「通知を繰り返さない」の判定が崩れる）．
        /// </summary>
        [Test]
        public void FormatCalibrationFailure_例外_検出中の差し替え文とは異なる() {
            var ex = new InvalidOperationException("理由X");

            var notice = CameraBase.FormatCalibrationFailure(ex);
            var retrying = CameraBase.FormatChartDetectionMessage(ex.Message, "舌診チャートの検出中");

            Assert.That(notice, Is.Not.EqualTo(retrying));
        }

        // ================================================================================
        // ShouldNotifyCalibrationFailure（同じ失敗を繰り返し通知しない）
        // ================================================================================

        /// <summary>未通知（直前の通知が null）なら通知する</summary>
        [Test]
        public void ShouldNotifyCalibrationFailure_未通知_trueを返す() {
            Assert.That(CameraBase.ShouldNotifyCalibrationFailure(null, "校正に失敗しました（A）"), Is.True);
        }

        /// <summary>直前に通知した内容と同じなら通知しない（毎フレーム同じ例外が起きても通知が出続けない）</summary>
        [Test]
        public void ShouldNotifyCalibrationFailure_直前と同じ内容_falseを返す() {
            Assert.That(CameraBase.ShouldNotifyCalibrationFailure("校正に失敗しました（A）", "校正に失敗しました（A）"), Is.False);
        }

        /// <summary>直前と異なる内容（別の理由の失敗）なら通知する</summary>
        [Test]
        public void ShouldNotifyCalibrationFailure_直前と異なる内容_trueを返す() {
            Assert.That(CameraBase.ShouldNotifyCalibrationFailure("校正に失敗しました（A）", "校正に失敗しました（B）"), Is.True);
        }

        // ================================================================================
        // FormatChartDetectionMessage（失敗表示の持続）
        // ================================================================================

        /// <summary>直前の校正失敗が無ければ，チャート検出の表示はそのまま出す</summary>
        [Test]
        public void FormatChartDetectionMessage_失敗なし_検出側の表示をそのまま返す() {
            Assert.That(CameraBase.FormatChartDetectionMessage(null, "舌診チャートの検出中"), Is.EqualTo("舌診チャートの検出中"));
        }

        /// <summary>直前の校正失敗があれば，検出中の表示を「校正に失敗しました（理由）．チャートの検出からやり直しています」に差し替える</summary>
        [Test]
        public void FormatChartDetectionMessage_失敗あり検出中_失敗の表示に差し替える() {
            Assert.That(CameraBase.FormatChartDetectionMessage("理由X", "舌診チャートの検出中"),
                Is.EqualTo("校正に失敗しました（理由X）" + RetryingSuffix));
        }

        /// <summary>直前の校正失敗があれば，チャート検出後の「校正中」の表示も差し替える（校正が完了するまで失敗を出し続ける）</summary>
        [Test]
        public void FormatChartDetectionMessage_失敗あり校正中_失敗の表示に差し替える() {
            Assert.That(CameraBase.FormatChartDetectionMessage("理由X", "舌診チャート検出　校正中"),
                Is.EqualTo("校正に失敗しました（理由X）" + RetryingSuffix));
        }

        /// <summary>チャートが上下逆の案内は，直前の校正失敗があっても差し替えない（設置し直しの案内のため．ユーザー合意済み）</summary>
        [Test]
        public void FormatChartDetectionMessage_失敗あり上下逆の案内_差し替えない() {
            Assert.That(CameraBase.FormatChartDetectionMessage("理由X", ChartDetector.UpsideDownMessage), Is.EqualTo(ChartDetector.UpsideDownMessage));
        }

        /// <summary>上下逆の案内の文言は従来どおり（ChartDetector の表示の特性化）</summary>
        [Test]
        public void UpsideDownMessage_文言_従来の表示と同じ() {
            Assert.That(ChartDetector.UpsideDownMessage, Is.EqualTo("舌診チャートが上下逆方向です"));
        }

        // ================================================================================
        // FormatCalibrationDone（校正画像の保存失敗の警告）
        // ================================================================================

        /// <summary>校正画像を保存できたときは従来どおり「自動校正完了」</summary>
        [Test]
        public void FormatCalibrationDone_保存成功_自動校正完了() {
            Assert.That(CameraBase.FormatCalibrationDone(true), Is.EqualTo("自動校正完了"));
        }

        /// <summary>校正画像の保存に失敗したときは「自動校正完了（校正画像の保存に失敗）」と警告を添える</summary>
        [Test]
        public void FormatCalibrationDone_保存失敗_保存失敗の警告を添える() {
            Assert.That(CameraBase.FormatCalibrationDone(false), Is.EqualTo("自動校正完了（校正画像の保存に失敗）"));
        }

        // ================================================================================
        // GetDrainIdleTimeout（保存キューの待ち時間: 30 秒と撮影間隔の 2 倍の大きい方）
        // ================================================================================

        /// <summary>単発撮影（間隔 0）は 30 秒</summary>
        [Test]
        public void GetDrainIdleTimeout_間隔0_30秒() {
            Assert.That(CameraBase.GetDrainIdleTimeout(0), Is.EqualTo(TimeSpan.FromSeconds(30)));
        }

        /// <summary>負の間隔（不正値）は 30 秒（下限を割らない）</summary>
        [Test]
        public void GetDrainIdleTimeout_負の間隔_30秒() {
            Assert.That(CameraBase.GetDrainIdleTimeout(-1000), Is.EqualTo(TimeSpan.FromSeconds(30)));
        }

        /// <summary>間隔 1000ms（2 倍が 30 秒未満）は 30 秒</summary>
        [Test]
        public void GetDrainIdleTimeout_間隔1000ms_30秒() {
            Assert.That(CameraBase.GetDrainIdleTimeout(1000), Is.EqualTo(TimeSpan.FromSeconds(30)));
        }

        /// <summary>間隔 15000ms（2 倍がちょうど 30 秒）は 30 秒（境界）</summary>
        [Test]
        public void GetDrainIdleTimeout_間隔15000ms_30秒() {
            Assert.That(CameraBase.GetDrainIdleTimeout(15000), Is.EqualTo(TimeSpan.FromSeconds(30)));
        }

        /// <summary>間隔 15001ms（2 倍が 30 秒をわずかに超える）は間隔の 2 倍（境界の直後）</summary>
        [Test]
        public void GetDrainIdleTimeout_間隔15001ms_間隔の2倍() {
            Assert.That(CameraBase.GetDrainIdleTimeout(15001), Is.EqualTo(TimeSpan.FromMilliseconds(30002)));
        }

        /// <summary>間隔 20000ms は間隔の 2 倍（40 秒）</summary>
        [Test]
        public void GetDrainIdleTimeout_間隔20000ms_40秒() {
            Assert.That(CameraBase.GetDrainIdleTimeout(20000), Is.EqualTo(TimeSpan.FromSeconds(40)));
        }

        /// <summary>間隔 int.MaxValue でも溢れず（負や 30 秒に戻らず），間隔の 2 倍になる</summary>
        [Test]
        public void GetDrainIdleTimeout_間隔intMaxValue_溢れずに間隔の2倍() {
            var expected = TimeSpan.FromTicks(2L * int.MaxValue * TimeSpan.TicksPerMillisecond);

            TimeSpan actual = TimeSpan.Zero;
            Assert.DoesNotThrow((Action)(() => actual = CameraBase.GetDrainIdleTimeout(int.MaxValue)));
            Assert.That(actual, Is.EqualTo(expected));
        }
    }
}
