"""
Step 17 — Transaction Atomicity Tests (R1 + R2)

R1 — Batch transactions for multi-statement repository writes:
- test_set_items_roundtrips_correctly: setItems persists items and they can be retrieved
- test_batch_constraint_violation_rolls_back: A mid-batch constraint error leaves DB unchanged
- test_set_exercises_roundtrips_correctly: comboRepository.setExercises is atomic
- test_set_sessions_roundtrips_correctly: collectionRepository.setSessions is atomic

R2 — Atomic full-DB restore:
- test_valid_restore_preserves_data: A valid backup restores all entities
- test_corrupt_restore_preserves_original_data: A partially corrupt import leaves original data intact
"""
import sqlite3
import tempfile
import os

import pytest
from playwright.sync_api import Page, expect


def _wait_for_db(page: Page, app_url: str):
    page.goto(app_url)
    page.wait_for_function(
        "document.documentElement.getAttribute('data-db-ready') === 'true'",
        timeout=15000,
    )


def _clean_all(page: Page):
    page.evaluate("""async () => {
        const exercises = await window.__repos.exercises.getAll()
        for (const e of exercises) await window.__repos.exercises.delete(e.id)
        const combos = await window.__repos.combos.getAll()
        for (const c of combos) await window.__repos.combos.delete(c.id)
        const sessions = await window.__repos.sessions.getAll()
        for (const s of sessions) await window.__repos.sessions.delete(s.id)
        const collections = await window.__repos.collections.getAll()
        for (const c of collections) await window.__repos.collections.delete(c.id)
    }""")


def _create_exercise(page: Page, title: str) -> str:
    return page.evaluate(
        """async ([title]) => {
            const userId = await window.__repos.settings.get('user_uuid')
            return window.__repos.exercises.create({
                title, description: '', asymmetric: false, alternate: false,
                image_urls: '[]', video_urls: '[]', default_reps: 10, default_weight: 0,
                created_by: userId
            })
        }""",
        [title],
    )


def _create_session(page: Page, title: str) -> str:
    return page.evaluate(
        """async ([title]) => {
            const userId = await window.__repos.settings.get('user_uuid')
            return window.__repos.sessions.create({title, description: '', created_by: userId})
        }""",
        [title],
    )


def _create_combo(page: Page, title: str) -> str:
    return page.evaluate(
        """async ([title]) => {
            const userId = await window.__repos.settings.get('user_uuid')
            return window.__repos.combos.create({
                title, description: '', type: 'NONE', timer_config: {}, created_by: userId
            })
        }""",
        [title],
    )


def _create_collection(page: Page, label: str) -> str:
    return page.evaluate(
        """async ([label]) => {
            const userId = await window.__repos.settings.get('user_uuid')
            return window.__repos.collections.create({label, created_by: userId})
        }""",
        [label],
    )


# ── R1 tests ──────────────────────────────────────────────────────────────────


def test_set_items_roundtrips_correctly(page: Page, app_url: str):
    """setItems atomically replaces session items; the full set is retrievable."""
    _wait_for_db(page, app_url)
    _clean_all(page)

    ex1 = _create_exercise(page, "Squat")
    ex2 = _create_exercise(page, "Lunge")
    sess_id = _create_session(page, "Leg Day")

    items = [
        {"item_id": ex1, "item_type": "exercise", "position": 0, "repetitions": 10, "weight": 0},
        {"item_id": ex2, "item_type": "exercise", "position": 1, "repetitions": 12, "weight": 0},
    ]
    page.evaluate(
        """async ([sessId, items]) => window.__repos.sessions.setItems(sessId, items)""",
        [sess_id, items],
    )

    result = page.evaluate(
        """async ([sessId]) => {
            const s = await window.__repos.sessions.getById(sessId)
            return s ? s.items : []
        }""",
        [sess_id],
    )

    assert len(result) == 2, f"Expected 2 items, got {len(result)}"
    assert result[0]["item_id"] == ex1
    assert result[1]["item_id"] == ex2


def test_batch_constraint_violation_rolls_back(page: Page, app_url: str):
    """A batch where any statement fails leaves the DB unchanged (full rollback)."""
    _wait_for_db(page, app_url)
    _clean_all(page)

    ex_id = _create_exercise(page, "Bench Press")

    # Build a batch: first a valid INSERT into exercise, then one that violates UNIQUE
    result = page.evaluate(
        """async ([exId]) => {
            try {
                await window.__db.batch([
                    {
                        sql: "INSERT INTO exercise (id, title, description, asymmetric, alternate, image_urls, video_urls, default_reps, default_weight, created_by) VALUES (?, ?, '', 0, 0, '[]', '[]', 5, 0, 'test')",
                        params: ['new-ex-1', 'New Exercise 1']
                    },
                    {
                        // UNIQUE violation: same title as existing exercise
                        sql: "INSERT INTO exercise (id, title, description, asymmetric, alternate, image_urls, video_urls, default_reps, default_weight, created_by) VALUES (?, ?, '', 0, 0, '[]', '[]', 5, 0, 'test')",
                        params: ['new-ex-2', 'Bench Press']
                    }
                ])
                return { threw: false }
            } catch (e) {
                return { threw: true, message: e.message }
            }
        }""",
        [ex_id],
    )

    assert result["threw"], f"Batch should have thrown, but got: {result}"

    # Verify neither the valid nor invalid row was committed
    count = page.evaluate(
        """async () => {
            const rows = await window.__db.selectAll('SELECT id FROM exercise')
            return rows.length
        }"""
    )
    assert count == 1, f"Only the original exercise should exist, got {count}"


def test_set_exercises_roundtrips_correctly(page: Page, app_url: str):
    """comboRepository.setExercises atomically replaces exercises on a combo."""
    _wait_for_db(page, app_url)
    _clean_all(page)

    ex1 = _create_exercise(page, "Pull-up")
    ex2 = _create_exercise(page, "Dip")
    combo_id = _create_combo(page, "Upper Pull")

    exercises = [
        {"exerciseId": ex1, "defaultReps": 8, "defaultWeight": 0},
        {"exerciseId": ex2, "defaultReps": 10, "defaultWeight": 0},
    ]
    page.evaluate(
        """async ([comboId, exercises]) => window.__repos.combos.setExercises(comboId, exercises)""",
        [combo_id, exercises],
    )

    result = page.evaluate(
        """async ([comboId]) => {
            const c = await window.__repos.combos.getById(comboId)
            return c ? c.exercises : []
        }""",
        [combo_id],
    )

    assert len(result) == 2, f"Expected 2 exercises in combo, got {len(result)}"


def test_set_sessions_roundtrips_correctly(page: Page, app_url: str):
    """collectionRepository.setSessions atomically replaces sessions on a collection."""
    _wait_for_db(page, app_url)
    _clean_all(page)

    s1 = _create_session(page, "Session Alpha")
    s2 = _create_session(page, "Session Beta")
    coll_id = _create_collection(page, "My Week")

    page.evaluate(
        """async ([collId, s1, s2]) => window.__repos.collections.setSessions(collId, [s1, s2])""",
        [coll_id, s1, s2],
    )

    result = page.evaluate(
        """async ([collId]) => {
            const c = await window.__repos.collections.getById(collId)
            return c ? c.sessions : []
        }""",
        [coll_id],
    )

    assert len(result) == 2, f"Expected 2 sessions in collection, got {len(result)}"


# ── R2 tests ──────────────────────────────────────────────────────────────────


def _build_corrupt_db_bytes(tmp_path: str) -> bytes:
    """Create a SQLite DB that passes validation but has an exercise row with NULL created_by.

    The live DB has `created_by TEXT NOT NULL`, so inserting this row during restore
    will trigger a NOT NULL constraint failure mid-transaction.
    """
    db_path = os.path.join(tmp_path, "corrupt_import.db")
    conn = sqlite3.connect(db_path)
    # Required tables (REQUIRED_TABLES = ['schema_version', 'settings'])
    conn.execute(
        "CREATE TABLE schema_version (version INTEGER PRIMARY KEY, applied_at TEXT)"
    )
    conn.execute("INSERT INTO schema_version VALUES (1, datetime('now'))")
    conn.execute("CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT)")
    # exercise table without NOT NULL on created_by (SQLite won't enforce it here)
    conn.execute("""CREATE TABLE exercise (
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL UNIQUE,
        description TEXT DEFAULT '',
        asymmetric INTEGER DEFAULT 0,
        alternate INTEGER DEFAULT 0,
        image_urls TEXT DEFAULT '[]',
        video_urls TEXT DEFAULT '[]',
        default_reps INTEGER DEFAULT 0,
        default_weight REAL DEFAULT 0.0,
        created_by TEXT
    )""")
    # Insert a row where created_by = NULL — will fail NOT NULL in the live schema
    conn.execute(
        "INSERT INTO exercise VALUES ('corrupt-id', 'Corrupt Exercise', '', 0, 0, '[]', '[]', 5, 0.0, NULL)"
    )
    conn.commit()
    conn.close()
    with open(db_path, "rb") as f:
        return f.read()


def test_valid_restore_preserves_data(page: Page, app_url: str, tmp_path):
    """A valid DB backup can be exported and re-imported without data loss."""
    _wait_for_db(page, app_url)
    _clean_all(page)

    ex_id = _create_exercise(page, "Deadlift")

    # Export the DB as bytes via the worker
    db_bytes_list = page.evaluate(
        """async () => {
            const bytes = await window.__db.exportDatabase()
            return Array.from(bytes)
        }"""
    )

    # Import the exported bytes back — should succeed
    result = page.evaluate(
        """async (bytes) => window.__db.importDatabase(new Uint8Array(bytes))""",
        db_bytes_list,
    )

    assert result.get("success") is True, f"Valid restore should succeed, got: {result}"

    # After restore the exercise should still exist
    exercises = page.evaluate(
        """async () => window.__repos.exercises.getAll()"""
    )
    ids = [e["id"] for e in exercises]
    assert ex_id in ids, f"Exercise {ex_id} should survive a valid restore"


def test_corrupt_restore_preserves_original_data(page: Page, app_url: str, tmp_path):
    """A restore that fails mid-way (constraint violation) leaves the original data intact."""
    _wait_for_db(page, app_url)
    _clean_all(page)

    # Create a known exercise in the live DB
    ex_id = _create_exercise(page, "Original Exercise")

    # Build a corrupt DB file and pass bytes into the browser
    corrupt_bytes = _build_corrupt_db_bytes(str(tmp_path))
    corrupt_list = list(corrupt_bytes)

    result = page.evaluate(
        """async (bytes) => {
            try {
                return await window.__db.importDatabase(new Uint8Array(bytes))
            } catch (e) {
                return { success: false, error: 'THREW', message: e.message }
            }
        }""",
        corrupt_list,
    )

    # The import should have failed
    assert result.get("success") is False, f"Corrupt import should fail, got: {result}"

    # Original exercise must still be present (transaction was rolled back)
    exercises = page.evaluate("""async () => window.__repos.exercises.getAll()""")
    ids = [e["id"] for e in exercises]
    assert ex_id in ids, (
        f"Original exercise should survive a failed restore. "
        f"Got exercises: {exercises}, result: {result}"
    )
    assert len(exercises) == 1, f"Only original exercise should remain, got: {exercises}"
