"""
AwoooP Phase 1 Schema Integration Tests
=========================================
Task 1.7：驗收 awooop_phase1_control_plane_2026-05-04.sql 的核心不變式

測試涵蓋：
1. awooop_projects 基本 CRUD（NOT NULL、CHECK 約束）
2. awooop_contract_revisions 不可變性（immutability trigger）
3. awooop_published_revisions VIEW 只看 published + active
4. awooop_active_revisions active_pointer_guard（不可指向非 active revision）
5. awooop_active_revisions 跨租戶指向防護
6. RLS fail-closed（跨 project SELECT 被拒絕）
7. awooop_contract_outbox FK 完整性

前置條件：
  awooop_phase1_control_plane_2026-05-04.sql 已執行（含種子資料）

執行方式：
  export TEST_DATABASE_URL="postgresql+asyncpg://awoooi:..@192.168.0.188:5432/awoooi_dev?ssl=disable"
  cd apps/api && pytest tests/integration/test_awooop_phase1_schema.py -v

2026-05-04 ogt + Claude Sonnet 4.6（ADR-118 Task 1.7）
"""

import hashlib
import json
import uuid

import pytest
import pytest_asyncio
from sqlalchemy import text
from sqlalchemy.exc import IntegrityError, ProgrammingError
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from tests.integration.conftest import DEV_DB_URL

# =============================================================================
# Fixtures
# =============================================================================


@pytest_asyncio.fixture
async def raw_conn():
    """原始 asyncpg 連線（for SET LOCAL / BYPASSRLS role 切換）"""
    engine = create_async_engine(DEV_DB_URL, echo=False)
    async with engine.connect() as conn:
        yield conn
    await engine.dispose()


def _sha256(body: dict) -> str:
    return hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()


# =============================================================================
# Helpers
# =============================================================================


async def _insert_project(conn, project_id: str, mode: str = "legacy_awoooi_default") -> None:
    await conn.execute(
        text("""
            INSERT INTO awooop_projects (project_id, display_name, migration_mode)
            VALUES (:pid, :name, :mode)
            ON CONFLICT (project_id) DO NOTHING
        """),
        {"pid": project_id, "name": project_id, "mode": mode},
    )


async def _insert_draft_revision(conn, project_id: str, contract_id: str = "agent:test") -> uuid.UUID:
    body = {"name": "test", "version": "1.0"}
    body_hash = _sha256(body)
    result = await conn.execute(
        text("""
            INSERT INTO awooop_contract_revisions
              (project_id, contract_family, contract_id, body_json, body_hash)
            VALUES (:pid, 'agent', :cid, :body::jsonb, :hash)
            RETURNING revision_id
        """),
        {
            "pid": project_id,
            "cid": contract_id,
            "body": json.dumps(body),
            "hash": body_hash,
        },
    )
    row = result.fetchone()
    return row[0]


# =============================================================================
# 1. awooop_projects CHECK 約束
# =============================================================================


class TestAwoooPProjects:
    async def test_seed_awoooi_exists(self, db_session: AsyncSession):
        result = await db_session.execute(
            text("SELECT project_id, migration_mode FROM awooop_projects WHERE project_id = 'awoooi'")
        )
        row = result.fetchone()
        assert row is not None, "種子資料 awoooi 必須存在"
        assert row[1] == "legacy_awoooi_default"

    async def test_invalid_migration_mode_rejected(self, db_session: AsyncSession):
        with pytest.raises((IntegrityError, Exception)):
            await db_session.execute(
                text("""
                    INSERT INTO awooop_projects (project_id, display_name, migration_mode)
                    VALUES ('test_bad_mode', 'Test', 'invalid_mode')
                """)
            )
            await db_session.flush()

    async def test_negative_budget_rejected(self, db_session: AsyncSession):
        with pytest.raises((IntegrityError, Exception)):
            await db_session.execute(
                text("""
                    INSERT INTO awooop_projects (project_id, display_name, budget_limit_usd)
                    VALUES ('test_neg_budget', 'Test', -1.00)
                """)
            )
            await db_session.flush()

    async def test_null_allowed_channels_rejected(self, db_session: AsyncSession):
        with pytest.raises((IntegrityError, Exception)):
            await db_session.execute(
                text("""
                    INSERT INTO awooop_projects (project_id, display_name, allowed_channels)
                    VALUES ('test_null_channels', 'Test', NULL)
                """)
            )
            await db_session.flush()


# =============================================================================
# 2. awooop_contract_revisions 不可變性
# =============================================================================


class TestRevisionImmutability:
    async def test_draft_body_can_be_updated(self, raw_conn):
        """draft 可以修改 body（尚未 publish）"""
        async with raw_conn.begin():
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('immut_test', 'Test') ON CONFLICT DO NOTHING"))
            body = {"v": 1}
            rev_id = (await raw_conn.execute(
                text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash) VALUES ('immut_test', 'agent', 'imm:1', :body::jsonb, :hash) RETURNING revision_id"),
                {"body": json.dumps(body), "hash": _sha256(body)},
            )).scalar()

            new_body = {"v": 2}
            # draft → 應允許修改 body
            await raw_conn.execute(
                text("UPDATE awooop_contract_revisions SET body_json = :b::jsonb, body_hash = :h WHERE revision_id = :rid"),
                {"b": json.dumps(new_body), "h": _sha256(new_body), "rid": rev_id},
            )

    async def test_published_body_immutable(self, raw_conn):
        """published 後 body_json 修改必須被 trigger 拒絕"""
        async with raw_conn.begin():
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('immut_pub', 'Test') ON CONFLICT DO NOTHING"))
            body = {"v": 1}
            rev_id = (await raw_conn.execute(
                text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash, lifecycle_status) VALUES ('immut_pub', 'agent', 'imm:pub', :body::jsonb, :hash, 'published') RETURNING revision_id"),
                {"body": json.dumps(body), "hash": _sha256(body)},
            )).scalar()

            with pytest.raises(Exception, match="immutable"):
                await raw_conn.execute(
                    text("UPDATE awooop_contract_revisions SET body_json = :b::jsonb WHERE revision_id = :rid"),
                    {"b": json.dumps({"v": 9}), "rid": rev_id},
                )

    async def test_identity_fields_always_immutable(self, raw_conn):
        """project_id/contract_family/contract_id 在任何 lifecycle 下都不可改"""
        async with raw_conn.begin():
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('immut_id', 'Test') ON CONFLICT DO NOTHING"))
            body = {"v": 1}
            rev_id = (await raw_conn.execute(
                text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash) VALUES ('immut_id', 'agent', 'imm:id', :body::jsonb, :hash) RETURNING revision_id"),
                {"body": json.dumps(body), "hash": _sha256(body)},
            )).scalar()

            with pytest.raises(Exception, match="immutable"):
                await raw_conn.execute(
                    text("UPDATE awooop_contract_revisions SET contract_family = 'mcp_gateway' WHERE revision_id = :rid"),
                    {"rid": rev_id},
                )

    async def test_invalid_lifecycle_transition_rejected(self, raw_conn):
        """非法 lifecycle 流轉（draft → revoked 跳過 published/active）被拒絕"""
        async with raw_conn.begin():
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('lc_test', 'Test') ON CONFLICT DO NOTHING"))
            body = {"v": 1}
            rev_id = (await raw_conn.execute(
                text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash) VALUES ('lc_test', 'agent', 'lc:1', :body::jsonb, :hash) RETURNING revision_id"),
                {"body": json.dumps(body), "hash": _sha256(body)},
            )).scalar()

            with pytest.raises(Exception, match="illegal lifecycle transition"):
                await raw_conn.execute(
                    text("UPDATE awooop_contract_revisions SET lifecycle_status = 'revoked' WHERE revision_id = :rid"),
                    {"rid": rev_id},
                )

    async def test_delete_revision_rejected(self, raw_conn):
        """DELETE 完全禁止（append-only）"""
        async with raw_conn.begin():
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('del_test', 'Test') ON CONFLICT DO NOTHING"))
            body = {"v": 1}
            rev_id = (await raw_conn.execute(
                text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash) VALUES ('del_test', 'agent', 'del:1', :body::jsonb, :hash) RETURNING revision_id"),
                {"body": json.dumps(body), "hash": _sha256(body)},
            )).scalar()

            with pytest.raises(Exception, match="append-only"):
                await raw_conn.execute(
                    text("DELETE FROM awooop_contract_revisions WHERE revision_id = :rid"),
                    {"rid": rev_id},
                )


# =============================================================================
# 3. awooop_published_revisions VIEW
# =============================================================================


class TestPublishedRevisionsView:
    async def test_draft_not_visible_in_view(self, raw_conn):
        """draft revision 不應出現在 awooop_published_revisions"""
        async with raw_conn.begin():
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('view_test', 'Test') ON CONFLICT DO NOTHING"))
            body = {"v": 1}
            rev_id = (await raw_conn.execute(
                text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash) VALUES ('view_test', 'agent', 'view:1', :body::jsonb, :hash) RETURNING revision_id"),
                {"body": json.dumps(body), "hash": _sha256(body)},
            )).scalar()

            count = (await raw_conn.execute(
                text("SELECT count(*) FROM awooop_published_revisions WHERE revision_id = :rid"),
                {"rid": rev_id},
            )).scalar()
            assert count == 0, "draft 不應出現在 published view"

    async def test_published_visible_in_view(self, raw_conn):
        """published revision 應出現在 awooop_published_revisions"""
        async with raw_conn.begin():
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('view_pub', 'Test') ON CONFLICT DO NOTHING"))
            body = {"v": 1}
            rev_id = (await raw_conn.execute(
                text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash, lifecycle_status) VALUES ('view_pub', 'agent', 'view:pub', :body::jsonb, :hash, 'published') RETURNING revision_id"),
                {"body": json.dumps(body), "hash": _sha256(body)},
            )).scalar()

            count = (await raw_conn.execute(
                text("SELECT count(*) FROM awooop_published_revisions WHERE revision_id = :rid"),
                {"rid": rev_id},
            )).scalar()
            assert count == 1, "published 應出現在 published view"


# =============================================================================
# 4. awooop_active_revisions — active_pointer_guard
# =============================================================================


class TestActivePointerGuard:
    async def test_cannot_point_to_draft_revision(self, raw_conn):
        """active pointer 不可指向 draft revision"""
        async with raw_conn.begin():
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('ptr_test', 'Test') ON CONFLICT DO NOTHING"))
            body = {"v": 1}
            rev_id = (await raw_conn.execute(
                text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash) VALUES ('ptr_test', 'agent', 'ptr:1', :body::jsonb, :hash) RETURNING revision_id"),
                {"body": json.dumps(body), "hash": _sha256(body)},
            )).scalar()

            with pytest.raises(Exception):
                await raw_conn.execute(
                    text("INSERT INTO awooop_active_revisions (project_id, contract_family, contract_id, active_revision_id) VALUES ('ptr_test', 'agent', 'ptr:1', :rid)"),
                    {"rid": rev_id},
                )

    async def test_can_point_to_active_revision(self, raw_conn):
        """active pointer 可以指向 lifecycle_status=active 的 revision"""
        async with raw_conn.begin():
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('ptr_ok', 'Test') ON CONFLICT DO NOTHING"))
            body = {"v": 1}
            rev_id = (await raw_conn.execute(
                text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash, lifecycle_status) VALUES ('ptr_ok', 'agent', 'ptr:ok', :body::jsonb, :hash, 'active') RETURNING revision_id"),
                {"body": json.dumps(body), "hash": _sha256(body)},
            )).scalar()

            await raw_conn.execute(
                text("INSERT INTO awooop_active_revisions (project_id, contract_family, contract_id, active_revision_id) VALUES ('ptr_ok', 'agent', 'ptr:ok', :rid)"),
                {"rid": rev_id},
            )

    async def test_cross_tenant_pointer_rejected(self, raw_conn):
        """active pointer 的 project_id 必須與 revision 的 project_id 相同"""
        async with raw_conn.begin():
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('tenant_a', 'Tenant A') ON CONFLICT DO NOTHING"))
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('tenant_b', 'Tenant B') ON CONFLICT DO NOTHING"))

            body = {"v": 1}
            rev_id = (await raw_conn.execute(
                text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash, lifecycle_status) VALUES ('tenant_a', 'agent', 'cross:1', :body::jsonb, :hash, 'active') RETURNING revision_id"),
                {"body": json.dumps(body), "hash": _sha256(body)},
            )).scalar()

            with pytest.raises(Exception, match="mismatch"):
                await raw_conn.execute(
                    text("INSERT INTO awooop_active_revisions (project_id, contract_family, contract_id, active_revision_id) VALUES ('tenant_b', 'agent', 'cross:1', :rid)"),
                    {"rid": rev_id},
                )


# =============================================================================
# 5. RLS fail-closed 驗證（需要 awooop_app role 可用）
# =============================================================================


class TestRLSFailClosed:
    async def test_no_project_id_set_returns_empty(self, raw_conn):
        """沒有 SET LOCAL app.project_id 時，awooop_app role 看不到任何資料"""
        try:
            async with raw_conn.begin():
                await raw_conn.execute(text("SET LOCAL ROLE awooop_app"))
                # 不設 app.project_id
                count = (await raw_conn.execute(
                    text("SELECT count(*) FROM awooop_contract_revisions")
                )).scalar()
                assert count == 0, "未設 app.project_id，應看不到任何資料（fail-closed）"
        except Exception as e:
            if "does not exist" in str(e) or "awooop_app" in str(e):
                pytest.skip("awooop_app role 尚未建立（migration 未執行）")
            raise

    async def test_wrong_project_id_returns_empty(self, raw_conn):
        """設定不存在的 project_id，應看不到任何資料"""
        try:
            async with raw_conn.begin():
                await raw_conn.execute(text("SET LOCAL ROLE awooop_app"))
                await raw_conn.execute(text("SET LOCAL app.project_id = 'nonexistent_tenant_xyz'"))
                count = (await raw_conn.execute(
                    text("SELECT count(*) FROM awooop_contract_revisions")
                )).scalar()
                assert count == 0, "不存在的 project_id 應看不到任何資料"
        except Exception as e:
            if "does not exist" in str(e) or "awooop_app" in str(e):
                pytest.skip("awooop_app role 尚未建立（migration 未執行）")
            raise

    async def test_correct_project_id_returns_own_data(self, raw_conn):
        """設定正確 project_id，應只看到自己的資料"""
        try:
            async with raw_conn.begin():
                # 先用 superuser 插入測試資料
                await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('rls_tenant_x', 'RLS Test X') ON CONFLICT DO NOTHING"))
                body = {"v": 1}
                await raw_conn.execute(
                    text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash) VALUES ('rls_tenant_x', 'agent', 'rls:1', :body::jsonb, :hash)"),
                    {"body": json.dumps(body), "hash": _sha256(body)},
                )

                # 切換到 awooop_app role 並設定 project_id
                await raw_conn.execute(text("SET LOCAL ROLE awooop_app"))
                await raw_conn.execute(text("SET LOCAL app.project_id = 'rls_tenant_x'"))
                count = (await raw_conn.execute(
                    text("SELECT count(*) FROM awooop_contract_revisions WHERE project_id = 'rls_tenant_x'")
                )).scalar()
                assert count >= 1, "應看到自己 tenant 的資料"
        except Exception as e:
            if "does not exist" in str(e) or "awooop_app" in str(e):
                pytest.skip("awooop_app role 尚未建立（migration 未執行）")
            raise


# =============================================================================
# 6. awooop_contract_outbox FK 完整性
# =============================================================================


class TestContractOutboxFK:
    async def test_outbox_event_requires_existing_project(self, raw_conn):
        """outbox 事件必須關聯到現有 project"""
        async with raw_conn.begin():
            body = {"v": 1}
            fake_rev = uuid.uuid4()
            with pytest.raises((IntegrityError, Exception)):
                await raw_conn.execute(
                    text("""
                        INSERT INTO awooop_contract_outbox
                          (event_type, project_id, contract_family, contract_id, new_revision_id)
                        VALUES ('contract.activated', 'nonexistent_xyz', 'agent', 'test', :rid)
                    """),
                    {"rid": fake_rev},
                )
                await raw_conn.flush()

    async def test_outbox_dedup_unique_constraint(self, raw_conn):
        """同一 revision 的同一 event_type 只能有一筆（防止重複投遞）"""
        async with raw_conn.begin():
            await raw_conn.execute(text("INSERT INTO awooop_projects (project_id, display_name) VALUES ('outbox_dedup', 'Test') ON CONFLICT DO NOTHING"))
            body = {"v": 1}
            rev_id = (await raw_conn.execute(
                text("INSERT INTO awooop_contract_revisions (project_id, contract_family, contract_id, body_json, body_hash) VALUES ('outbox_dedup', 'agent', 'ob:1', :body::jsonb, :hash) RETURNING revision_id"),
                {"body": json.dumps(body), "hash": _sha256(body)},
            )).scalar()

            await raw_conn.execute(
                text("INSERT INTO awooop_contract_outbox (event_type, project_id, contract_family, contract_id, new_revision_id) VALUES ('contract.activated', 'outbox_dedup', 'agent', 'ob:1', :rid)"),
                {"rid": rev_id},
            )
            with pytest.raises((IntegrityError, Exception)):
                await raw_conn.execute(
                    text("INSERT INTO awooop_contract_outbox (event_type, project_id, contract_family, contract_id, new_revision_id) VALUES ('contract.activated', 'outbox_dedup', 'agent', 'ob:1', :rid)"),
                    {"rid": rev_id},
                )
                await raw_conn.flush()
