"""記事B 最小実験：要件から作った期待結果で、承認処理を検証する（改訂版 rev1）。

使い方（このフォルダで実行）:
    python3 test_approval.py approval_v1              # 説明用の失敗例 → 失敗するテストが出る（期待どおり）
    python3 test_approval.py approval_v2              # 修正後 → 全部通ることを期待
    python3 test_approval.py approval_v2 --suite original    # 元の8シナリオ（T1〜T8、検査を強化）だけ
    python3 test_approval.py approval_v2 --suite additional  # 追加の確認（T9〜T12）だけ

期待結果は要件（設計書 5.4 REQ-003〜007）から先に決めた。実装の出力に合わせて書き換えてはいけない。

改訂版で強化した検査（GPTレビュー R3 対応）:
  - 拒否のときは「承認履歴が増えない」だけでなく、依頼と履歴の全行が操作前と同一であることを確かめる。
  - 成功のときは、履歴の操作・操作者・時刻・理由が期待どおりであることを確かめる。時刻は固定時刻を注入する。
  - 途中失敗のときは、状態と履歴の両方が元どおりであることを確かめる。
  - 各テストの終わりに接続を閉じる。

assertion（アサーション）とは「この条件が成り立つはず」と書いた検査で、成り立たなければテストが失敗する。
"""
import importlib
import sys
import unittest

from common import fresh_connection, state_of, history_of, full_snapshot

MODULE_NAME = "approval_v2"  # 引数で上書き
FIXED_TIME = "2026-09-18T12:00:00+00:00"


class _Base(unittest.TestCase):
    def setUp(self):
        self.mod = importlib.import_module(MODULE_NAME)
        self.mod.clock = lambda: FIXED_TIME   # 実行時刻の揺れで落ちないよう固定時刻を注入
        self.conn = fresh_connection()

    def tearDown(self):
        self.mod.clock = None
        self.conn.close()

    def assertUnchanged(self, before, msg="拒否されたのに依頼または履歴が変わっている"):
        self.assertEqual(full_snapshot(self.conn), before, msg)


class OriginalScenarios(_Base):
    """元の8シナリオ（T1〜T8）。検査内容は改訂版で強化している。"""

    # T1 正常な承認 ---------------------------------------------------
    def test_T1_normal_approval(self):
        """割当承認者が提出済みの依頼を承認 → 承認済み。履歴に承認・鈴木・固定時刻が1件（REQ-003, 005）"""
        before_hist = history_of(self.conn, "REQ-A01")
        r = self.mod.approve(self.conn, "REQ-A01", "u-suzuki")
        self.assertTrue(r.ok, r.reason)
        self.assertEqual(state_of(self.conn, "REQ-A01"), "承認済み")
        after_hist = history_of(self.conn, "REQ-A01")
        self.assertEqual(after_hist[:-1], before_hist, "既存の履歴が変わっている")
        self.assertEqual(after_hist[-1], ("承認", "u-suzuki", FIXED_TIME, None))

    # T2 無権限（割当外） ---------------------------------------------
    def test_T2_not_assigned_approver(self):
        """割当外の承認者（高橋）が承認 → 拒否。依頼・履歴の全行が不変（REQ-003）"""
        before = full_snapshot(self.conn)
        r = self.mod.approve(self.conn, "REQ-A01", "u-takahashi")
        self.assertFalse(r.ok)
        self.assertUnchanged(before)

    # T3 自己承認 ------------------------------------------------------
    def test_T3_self_approval(self):
        """依頼者=承認者の異常データを本人が承認 → 拒否。全行不変（REQ-003）"""
        before = full_snapshot(self.conn)
        r = self.mod.approve(self.conn, "REQ-A03", "u-suzuki")
        self.assertFalse(r.ok)
        self.assertUnchanged(before)

    # T4 対象外の状態 --------------------------------------------------
    def test_T4_wrong_state_draft(self):
        """下書きの依頼を割当承認者が承認 → 拒否。全行不変（REQ-003）"""
        before = full_snapshot(self.conn)
        r = self.mod.approve(self.conn, "REQ-A02", "u-suzuki")
        self.assertFalse(r.ok)
        self.assertUnchanged(before)

    # T5 重複 ----------------------------------------------------------
    def test_T5_duplicate_approval(self):
        """同じ承認が2回届く → 2回目は拒否。1回目の後から何も変わらない。承認履歴は1件（REQ-006）"""
        r1 = self.mod.approve(self.conn, "REQ-A01", "u-suzuki")
        self.assertTrue(r1.ok)
        after_first = full_snapshot(self.conn)
        r2 = self.mod.approve(self.conn, "REQ-A01", "u-suzuki")
        self.assertFalse(r2.ok)
        self.assertUnchanged(after_first, "2回目の承認で何かが変わっている")
        self.assertEqual(len(history_of(self.conn, "REQ-A01", "承認")), 1)

    # T6 途中失敗 ------------------------------------------------------
    def test_T6_partial_failure_rolls_back(self):
        """状態更新の直後に失敗 → 状態と履歴の両方が元どおり（REQ-007）"""
        before = full_snapshot(self.conn)

        def boom():
            raise RuntimeError("履歴の保存に失敗した（模擬）")
        try:
            r = self.mod.approve(self.conn, "REQ-A01", "u-suzuki", _fault=boom)
            self.assertFalse(r.ok)
        except RuntimeError:
            pass  # 例外がそのまま外へ出る実装でも、保存内容の検査は続ける
        self.assertUnchanged(before, "途中失敗なのに状態変更または履歴が残っている")

    # T7 差戻し理由なし ------------------------------------------------
    def test_T7_send_back_requires_reason(self):
        """理由なしの差戻し → 拒否。全行不変（REQ-004）"""
        before = full_snapshot(self.conn)
        r = self.mod.send_back(self.conn, "REQ-A01", "u-suzuki", "")
        self.assertFalse(r.ok)
        self.assertUnchanged(before)

    # T8 差戻し正常 ----------------------------------------------------
    def test_T8_send_back_with_reason(self):
        """理由ありの差戻し → 差戻し。履歴に差戻し・鈴木・固定時刻・理由が1件（REQ-004, 005）"""
        before_hist = history_of(self.conn, "REQ-A01")
        r = self.mod.send_back(self.conn, "REQ-A01", "u-suzuki", "日時の記載が不足")
        self.assertTrue(r.ok, r.reason)
        self.assertEqual(state_of(self.conn, "REQ-A01"), "差戻し")
        after_hist = history_of(self.conn, "REQ-A01")
        self.assertEqual(after_hist[:-1], before_hist, "既存の履歴が変わっている")
        self.assertEqual(after_hist[-1], ("差戻し", "u-suzuki", FIXED_TIME, "日時の記載が不足"))


class AdditionalChecks(_Base):
    """改訂版で追加した確認（T9〜T12）。"""

    # T9 役割 ----------------------------------------------------------
    def test_T9_assignee_without_approver_role(self):
        """承認者でない運用管理者が割当先になっている異常データ → 拒否。全行不変（REQ-003：役割と割当は別条件）"""
        before = full_snapshot(self.conn)
        r = self.mod.approve(self.conn, "REQ-A05", "u-admin")
        self.assertFalse(r.ok)
        self.assertUnchanged(before)

    # T10 外部キー ------------------------------------------------------
    def test_T10_foreign_key_enforced(self):
        """存在しない依頼IDの履歴を直接追加 → 保存先が拒否する（宣言した関係を接続が守っている）"""
        import sqlite3
        self.assertEqual(self.conn.execute("PRAGMA foreign_keys").fetchone()[0], 1)
        with self.assertRaises(sqlite3.IntegrityError):
            self.conn.execute(
                "INSERT INTO history (request_id, action, actor_id, at) VALUES ('REQ-NONE', '承認', 'u-suzuki', ?)",
                (FIXED_TIME,),
            )
        self.conn.rollback()

    # T11 保存先の条件 --------------------------------------------------
    def test_T11_sql_state_guard(self):
        """処理側の状態確認を通り抜けた場合でも、UPDATE の状態条件と更新件数の検査で止まる（REQ-003/006 の二重の守り）。
        処理側の確認を一時的に素通りさせて、下書きの依頼を承認しようとする。"""
        if not hasattr(self.mod, "_check_approver"):
            self.skipTest("この版には処理側の確認関数がない")
        before = full_snapshot(self.conn)
        original = self.mod._check_approver
        self.mod._check_approver = lambda conn, rid, aid: (("下書き", "u-sato", "u-suzuki"), None)
        try:
            r = self.mod.approve(self.conn, "REQ-A02", "u-suzuki")
        finally:
            self.mod._check_approver = original
        self.assertFalse(r.ok)
        self.assertUnchanged(before, "保存先の条件で止まらず、下書きが変更された")

    # T12 接続設定 ------------------------------------------------------
    def test_T12_autocommit_connection_refused(self):
        """自動確定モードの接続 → 変更前に拒否。全行不変（取消を保証できない設定では処理しない）"""
        self.conn.isolation_level = None
        before = full_snapshot(self.conn)
        r = self.mod.approve(self.conn, "REQ-A01", "u-suzuki")
        self.assertFalse(r.ok)
        self.assertUnchanged(before, "自動確定モードで処理が進んだ")


def _suite(name):
    loader = unittest.TestLoader()
    if name == "original":
        return loader.loadTestsFromTestCase(OriginalScenarios)
    if name == "additional":
        return loader.loadTestsFromTestCase(AdditionalChecks)
    s = unittest.TestSuite()
    s.addTests(loader.loadTestsFromTestCase(OriginalScenarios))
    s.addTests(loader.loadTestsFromTestCase(AdditionalChecks))
    return s


if __name__ == "__main__":
    args = sys.argv[1:]
    suite_name = "all"
    if "--suite" in args:
        i = args.index("--suite")
        suite_name = args[i + 1]
        del args[i:i + 2]
    if args:
        MODULE_NAME = args[0]
    print(f"=== 検証対象: {MODULE_NAME} / スイート: {suite_name} ===", flush=True)
    result = unittest.TextTestRunner(verbosity=2).run(_suite(suite_name))
    print(f"結果: 実行 {result.testsRun} 件 / 失敗 {len(result.failures)} / エラー {len(result.errors)} / スキップ {len(result.skipped)}", flush=True)
    sys.exit(0 if result.wasSuccessful() else 1)
