"""Unit tests for auth.models V3 — department-scoped admin tables (U1).

Covers:
- ``init_auth_db`` creates the new V3 tables (departments, user_departments,
  department_skill_bindings, department_kb_bindings, department_quotas)
- ``init_auth_db`` is idempotent (calling twice does not error)
- ``_SCHEMA_VERSION`` is recorded as 4 in ``auth_meta`` (V4 adds skill_states)
- ``departments`` insert + query round-trip
- ``user_departments`` many-to-many relationship (one user → many departments,
  one department → many users)
- ``department_skill_bindings`` UNIQUE (department_id, skill_name) constraint
- ``department_quotas`` UNIQUE (department_id, quota_type, period) constraint
- ``department_row_to_dict`` / ``user_department_row_to_dict`` helpers
- Indexes are created for the common access patterns
"""

from __future__ import annotations

import sqlite3
import uuid
from datetime import datetime, timezone
from pathlib import Path

import aiosqlite
import pytest

from agentkit.server.auth.models import (
    DepartmentKbBindingModel,
    DepartmentModel,
    DepartmentQuotaModel,
    DepartmentSkillBindingModel,
    UserDepartmentModel,
    _SCHEMA_VERSION,
    department_row_to_dict,
    init_auth_db,
    user_department_row_to_dict,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
async def fresh_db(tmp_path: Path) -> Path:
    """A brand-new auth DB on a fresh path (no data)."""
    db_path = tmp_path / "auth.db"
    await init_auth_db(db_path)
    return db_path


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


async def _insert_department(
    db: aiosqlite.Connection,
    *,
    dept_id: str | None = None,
    name: str | None = None,
    description: str | None = None,
    is_active: bool = True,
) -> str:
    """Insert a minimal department row and return its id."""
    dept_id = dept_id or str(uuid.uuid4())
    name = name or f"dept-{dept_id[:8]}"
    await db.execute(
        "INSERT INTO departments (id, name, description, is_active, created_at) "
        "VALUES (?, ?, ?, ?, ?)",
        (dept_id, name, description, 1 if is_active else 0, _now_iso()),
    )
    return dept_id


async def _insert_user(db: aiosqlite.Connection, *, user_id: str | None = None) -> str:
    """Insert a minimal user row and return its id."""
    user_id = user_id or str(uuid.uuid4())
    now_iso = _now_iso()
    await db.execute(
        "INSERT INTO users "
        "(id, username, email, password_hash, role, is_active, "
        " is_terminal_authorized, is_server_terminal_authorized, "
        " created_at, updated_at) "
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
        (
            user_id,
            f"user-{user_id[:8]}",
            f"{user_id[:8]}@example.com",
            "$2b$12$placeholder.hash.placeholder.hash.placeholder.hash",
            "member",
            1,
            0,
            0,
            now_iso,
            now_iso,
        ),
    )
    return user_id


async def _list_index_names(db: aiosqlite.Connection, table: str) -> set[str]:
    """Return the set of index names for a table."""
    db.row_factory = aiosqlite.Row
    cursor = await db.execute(f"PRAGMA index_list({table})")
    rows = await cursor.fetchall()
    return {row["name"] for row in rows}


async def _list_table_names(db: aiosqlite.Connection) -> set[str]:
    """Return the set of table names in the SQLite file."""
    cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table'")
    rows = await cursor.fetchall()
    return {row[0] for row in rows}


# ---------------------------------------------------------------------------
# _SCHEMA_VERSION
# ---------------------------------------------------------------------------


class TestSchemaVersion:
    def test_schema_version_is_v4(self):
        """V4 adds the skill_states table (U6 — Admin Console)."""
        assert _SCHEMA_VERSION == 4

    def test_sqlalchemy_model_table_names(self):
        assert DepartmentModel.__tablename__ == "departments"
        assert UserDepartmentModel.__tablename__ == "user_departments"
        assert DepartmentSkillBindingModel.__tablename__ == "department_skill_bindings"
        assert DepartmentKbBindingModel.__tablename__ == "department_kb_bindings"
        assert DepartmentQuotaModel.__tablename__ == "department_quotas"


# ---------------------------------------------------------------------------
# init_auth_db: table creation + idempotency
# ---------------------------------------------------------------------------


class TestInitAuthDbTables:
    async def test_creates_departments_table(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            tables = await _list_table_names(db)
        assert "departments" in tables

    async def test_creates_user_departments_table(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            tables = await _list_table_names(db)
        assert "user_departments" in tables

    async def test_creates_department_skill_bindings_table(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            tables = await _list_table_names(db)
        assert "department_skill_bindings" in tables

    async def test_creates_department_kb_bindings_table(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            tables = await _list_table_names(db)
        assert "department_kb_bindings" in tables

    async def test_creates_department_quotas_table(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            tables = await _list_table_names(db)
        assert "department_quotas" in tables

    async def test_records_schema_version_4_in_auth_meta(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            db.row_factory = aiosqlite.Row
            cursor = await db.execute("SELECT value FROM auth_meta WHERE key='schema_version'")
            row = await cursor.fetchone()
        assert row is not None
        assert row["value"] == "4"
        assert row["value"] == str(_SCHEMA_VERSION)

    async def test_init_auth_db_is_idempotent(self, tmp_path: Path):
        """Calling init_auth_db twice on the same path must not error."""
        db_path = tmp_path / "auth.db"
        await init_auth_db(db_path)
        # Second call should be a no-op (CREATE TABLE IF NOT EXISTS + idempotent
        # meta upsert). Must not raise.
        await init_auth_db(db_path)

        async with aiosqlite.connect(str(db_path)) as db:
            tables = await _list_table_names(db)
        assert "departments" in tables
        assert "user_departments" in tables


# ---------------------------------------------------------------------------
# Indexes
# ---------------------------------------------------------------------------


class TestDepartmentIndexes:
    async def test_user_departments_user_id_index(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            indexes = await _list_index_names(db, "user_departments")
        assert "idx_user_departments_user_id" in indexes

    async def test_user_departments_department_id_index(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            indexes = await _list_index_names(db, "user_departments")
        assert "idx_user_departments_department_id" in indexes

    async def test_department_skill_bindings_department_id_index(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            indexes = await _list_index_names(db, "department_skill_bindings")
        assert "idx_department_skill_bindings_department_id" in indexes

    async def test_department_kb_bindings_department_id_index(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            indexes = await _list_index_names(db, "department_kb_bindings")
        assert "idx_department_kb_bindings_department_id" in indexes

    async def test_department_quotas_department_id_index(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            indexes = await _list_index_names(db, "department_quotas")
        assert "idx_department_quotas_department_id" in indexes


# ---------------------------------------------------------------------------
# departments: insert + query
# ---------------------------------------------------------------------------


class TestDepartmentsCrud:
    async def test_insert_and_query_department(self, fresh_db: Path):
        dept_id = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_department(
                db,
                dept_id=dept_id,
                name="Engineering",
                description="Software engineering department",
            )
            await db.commit()
            db.row_factory = aiosqlite.Row
            cursor = await db.execute("SELECT * FROM departments WHERE id=?", (dept_id,))
            row = await cursor.fetchone()

        assert row is not None
        assert row["id"] == dept_id
        assert row["name"] == "Engineering"
        assert row["description"] == "Software engineering department"
        assert bool(row["is_active"]) is True

    async def test_department_name_is_unique(self, fresh_db: Path):
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_department(db, name="HR", description="Human Resources")
            await db.commit()
            # Inserting a second department with the same name must fail.
            with pytest.raises(sqlite3.IntegrityError):
                await _insert_department(db, name="HR", description="Duplicate")
                await db.commit()

    async def test_department_is_active_defaults_to_true(self, fresh_db: Path):
        """Insert without is_active → column should default to 1 (True)."""
        dept_id = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await db.execute(
                "INSERT INTO departments (id, name, created_at) VALUES (?, ?, ?)",
                (dept_id, "DefaultActive", _now_iso()),
            )
            await db.commit()
            db.row_factory = aiosqlite.Row
            cursor = await db.execute("SELECT is_active FROM departments WHERE id=?", (dept_id,))
            row = await cursor.fetchone()
        assert row is not None
        assert bool(row["is_active"]) is True

    async def test_department_description_is_nullable(self, fresh_db: Path):
        dept_id = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await db.execute(
                "INSERT INTO departments (id, name, created_at) VALUES (?, ?, ?)",
                (dept_id, "NoDescription", _now_iso()),
            )
            await db.commit()
            db.row_factory = aiosqlite.Row
            cursor = await db.execute("SELECT description FROM departments WHERE id=?", (dept_id,))
            row = await cursor.fetchone()
        assert row is not None
        assert row["description"] is None


# ---------------------------------------------------------------------------
# user_departments: many-to-many relationship
# ---------------------------------------------------------------------------


class TestUserDepartmentsManyToMany:
    async def test_user_can_belong_to_multiple_departments(self, fresh_db: Path):
        user_id = str(uuid.uuid4())
        dept_a = str(uuid.uuid4())
        dept_b = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_user(db, user_id=user_id)
            await _insert_department(db, dept_id=dept_a, name="DeptA")
            await _insert_department(db, dept_id=dept_b, name="DeptB")
            now = _now_iso()
            await db.executemany(
                "INSERT INTO user_departments (user_id, department_id, created_at) "
                "VALUES (?, ?, ?)",
                [(user_id, dept_a, now), (user_id, dept_b, now)],
            )
            await db.commit()
            db.row_factory = aiosqlite.Row
            cursor = await db.execute(
                "SELECT department_id FROM user_departments WHERE user_id=? ORDER BY department_id",
                (user_id,),
            )
            rows = await cursor.fetchall()

        dept_ids = [row["department_id"] for row in rows]
        assert dept_ids == sorted([dept_a, dept_b])

    async def test_department_can_have_multiple_users(self, fresh_db: Path):
        dept_id = str(uuid.uuid4())
        user_a = str(uuid.uuid4())
        user_b = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_department(db, dept_id=dept_id, name="Shared")
            await _insert_user(db, user_id=user_a)
            await _insert_user(db, user_id=user_b)
            now = _now_iso()
            await db.executemany(
                "INSERT INTO user_departments (user_id, department_id, created_at) "
                "VALUES (?, ?, ?)",
                [(user_a, dept_id, now), (user_b, dept_id, now)],
            )
            await db.commit()
            db.row_factory = aiosqlite.Row
            cursor = await db.execute(
                "SELECT user_id FROM user_departments WHERE department_id=? ORDER BY user_id",
                (dept_id,),
            )
            rows = await cursor.fetchall()

        user_ids = [row["user_id"] for row in rows]
        assert user_ids == sorted([user_a, user_b])

    async def test_composite_pk_prevents_duplicate_pair(self, fresh_db: Path):
        """The (user_id, department_id) composite PK rejects duplicate pairs."""
        user_id = str(uuid.uuid4())
        dept_id = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_user(db, user_id=user_id)
            await _insert_department(db, dept_id=dept_id, name="Unique")
            now = _now_iso()
            await db.execute(
                "INSERT INTO user_departments (user_id, department_id, created_at) "
                "VALUES (?, ?, ?)",
                (user_id, dept_id, now),
            )
            await db.commit()
            with pytest.raises(sqlite3.IntegrityError):
                await db.execute(
                    "INSERT INTO user_departments (user_id, department_id, created_at) "
                    "VALUES (?, ?, ?)",
                    (user_id, dept_id, now),
                )
                await db.commit()


# ---------------------------------------------------------------------------
# department_skill_bindings: UNIQUE constraint
# ---------------------------------------------------------------------------


class TestDepartmentSkillBindingsUnique:
    async def test_unique_department_skill_pair(self, fresh_db: Path):
        dept_id = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_department(db, dept_id=dept_id, name="Bindings")
            now = _now_iso()
            await db.execute(
                "INSERT INTO department_skill_bindings "
                "(id, department_id, skill_name, created_at) VALUES (?, ?, ?, ?)",
                (str(uuid.uuid4()), dept_id, "code_review", now),
            )
            await db.commit()
            # Same (department_id, skill_name) pair must fail, even with a new id.
            with pytest.raises(sqlite3.IntegrityError):
                await db.execute(
                    "INSERT INTO department_skill_bindings "
                    "(id, department_id, skill_name, created_at) VALUES (?, ?, ?, ?)",
                    (str(uuid.uuid4()), dept_id, "code_review", now),
                )
                await db.commit()

    async def test_same_skill_name_in_different_departments_is_allowed(self, fresh_db: Path):
        dept_a = str(uuid.uuid4())
        dept_b = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_department(db, dept_id=dept_a, name="DeptA")
            await _insert_department(db, dept_id=dept_b, name="DeptB")
            now = _now_iso()
            await db.executemany(
                "INSERT INTO department_skill_bindings "
                "(id, department_id, skill_name, created_at) VALUES (?, ?, ?, ?)",
                [
                    (str(uuid.uuid4()), dept_a, "shared_skill", now),
                    (str(uuid.uuid4()), dept_b, "shared_skill", now),
                ],
            )
            await db.commit()
            db.row_factory = aiosqlite.Row
            cursor = await db.execute(
                "SELECT COUNT(*) AS c FROM department_skill_bindings "
                "WHERE skill_name='shared_skill'"
            )
            row = await cursor.fetchone()
        assert row["c"] == 2


# ---------------------------------------------------------------------------
# department_quotas: UNIQUE constraint
# ---------------------------------------------------------------------------


class TestDepartmentQuotasUnique:
    async def test_unique_department_quota_type_period(self, fresh_db: Path):
        dept_id = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_department(db, dept_id=dept_id, name="Quota")
            now = _now_iso()
            await db.execute(
                "INSERT INTO department_quotas "
                "(id, department_id, quota_type, limit_value, period, updated_at) "
                "VALUES (?, ?, ?, ?, ?, ?)",
                (str(uuid.uuid4()), dept_id, "token_limit", "10000", "daily", now),
            )
            await db.commit()
            # Same (department_id, quota_type, period) triple must fail.
            with pytest.raises(sqlite3.IntegrityError):
                await db.execute(
                    "INSERT INTO department_quotas "
                    "(id, department_id, quota_type, limit_value, period, updated_at) "
                    "VALUES (?, ?, ?, ?, ?, ?)",
                    (str(uuid.uuid4()), dept_id, "token_limit", "20000", "daily", now),
                )
                await db.commit()

    async def test_same_quota_type_different_period_is_allowed(self, fresh_db: Path):
        dept_id = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_department(db, dept_id=dept_id, name="QuotaPeriods")
            now = _now_iso()
            await db.executemany(
                "INSERT INTO department_quotas "
                "(id, department_id, quota_type, limit_value, period, updated_at) "
                "VALUES (?, ?, ?, ?, ?, ?)",
                [
                    (str(uuid.uuid4()), dept_id, "token_limit", "10000", "daily", now),
                    (str(uuid.uuid4()), dept_id, "token_limit", "300000", "monthly", now),
                ],
            )
            await db.commit()
            db.row_factory = aiosqlite.Row
            cursor = await db.execute(
                "SELECT period, limit_value FROM department_quotas "
                "WHERE department_id=? AND quota_type='token_limit' "
                "ORDER BY period",
                (dept_id,),
            )
            rows = await cursor.fetchall()
        assert len(rows) == 2
        assert rows[0]["period"] == "daily"
        assert rows[0]["limit_value"] == "10000"
        assert rows[1]["period"] == "monthly"
        assert rows[1]["limit_value"] == "300000"

    async def test_quota_period_defaults_to_daily(self, fresh_db: Path):
        dept_id = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_department(db, dept_id=dept_id, name="DefaultPeriod")
            await db.execute(
                "INSERT INTO department_quotas "
                "(id, department_id, quota_type, limit_value, updated_at) "
                "VALUES (?, ?, ?, ?, ?)",
                (str(uuid.uuid4()), dept_id, "cost_limit", "10.00", _now_iso()),
            )
            await db.commit()
            db.row_factory = aiosqlite.Row
            cursor = await db.execute(
                "SELECT period FROM department_quotas WHERE department_id=?",
                (dept_id,),
            )
            row = await cursor.fetchone()
        assert row is not None
        assert row["period"] == "daily"


# ---------------------------------------------------------------------------
# row_to_dict helpers
# ---------------------------------------------------------------------------


class TestRowToDictHelpers:
    async def test_department_row_to_dict(self, fresh_db: Path):
        dept_id = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_department(
                db,
                dept_id=dept_id,
                name="HelperTest",
                description="Testing the helper",
                is_active=False,
            )
            await db.commit()
            db.row_factory = aiosqlite.Row
            cursor = await db.execute("SELECT * FROM departments WHERE id=?", (dept_id,))
            row = await cursor.fetchone()

        d = department_row_to_dict(row)
        assert d["id"] == dept_id
        assert d["name"] == "HelperTest"
        assert d["description"] == "Testing the helper"
        assert isinstance(d["is_active"], bool)
        assert d["is_active"] is False
        assert "created_at" in d

    async def test_department_row_to_dict_normalizes_is_active(self, fresh_db: Path):
        """DB stores 0/1; helper should return Python bool."""
        dept_id = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_department(db, dept_id=dept_id, name="BoolCheck", is_active=True)
            await db.commit()
            db.row_factory = aiosqlite.Row
            cursor = await db.execute("SELECT * FROM departments WHERE id=?", (dept_id,))
            row = await cursor.fetchone()
        d = department_row_to_dict(row)
        assert isinstance(d["is_active"], bool)
        assert d["is_active"] is True

    async def test_user_department_row_to_dict(self, fresh_db: Path):
        user_id = str(uuid.uuid4())
        dept_id = str(uuid.uuid4())
        async with aiosqlite.connect(str(fresh_db)) as db:
            await _insert_user(db, user_id=user_id)
            await _insert_department(db, dept_id=dept_id, name="UserDeptHelper")
            now = _now_iso()
            await db.execute(
                "INSERT INTO user_departments (user_id, department_id, created_at) "
                "VALUES (?, ?, ?)",
                (user_id, dept_id, now),
            )
            await db.commit()
            db.row_factory = aiosqlite.Row
            cursor = await db.execute(
                "SELECT * FROM user_departments WHERE user_id=? AND department_id=?",
                (user_id, dept_id),
            )
            row = await cursor.fetchone()

        d = user_department_row_to_dict(row)
        assert d["user_id"] == user_id
        assert d["department_id"] == dept_id
        assert d["created_at"] == now
