"""
Adversarial Stress Test Suite for FreeExile 30 Map Styles Database & Service.
Verifies WAL concurrency, boundary edge cases, SQL injection immunity,
schema CHECK constraints, and missing/corrupted database recovery.
"""

from __future__ import annotations
import concurrent.futures
import os
import shutil
import sqlite3
import tempfile
from pathlib import Path
from typing import Generator, List

import pytest

from server.world.map_style_schema import init_db
from server.world.map_style_service import MapStyleService
from server.world.map_style_types import MapStyleDefinition


@pytest.fixture(scope="module")
def canonical_db_path() -> Path:
    """Return canonical path to data/map_styles.db."""
    return Path(__file__).resolve().parent.parent.parent / "data" / "map_styles.db"


@pytest.fixture(scope="module")
def service(canonical_db_path: Path) -> MapStyleService:
    """Provide service connected to canonical database."""
    return MapStyleService(db_path=canonical_db_path)


@pytest.fixture
def isolated_temp_db() -> Generator[str, None, None]:
    """Provide isolated temporary directory and database path for mutation testing."""
    tmpdir = tempfile.mkdtemp(prefix="fe_adv_test_")
    db_file = os.path.join(tmpdir, "isolated_map_styles.db")
    yield db_file
    # Cleanup directory
    shutil.rmtree(tmpdir, ignore_errors=True)


class TestMapStyleWALConcurrency:
    """Adversarial testing of SQLite WAL mode and multi-threaded concurrency."""

    def test_high_volume_concurrent_readers(self, service: MapStyleService) -> None:
        """50 worker threads querying styles, tiles, and props concurrently."""
        def reader_task(worker_id: int) -> bool:
            code = (worker_id % 30) + 1
            s = service.get_style_by_code(code)
            assert s is not None and s.biome_code == code
            tiles = service.get_tiles_for_style(s.style_id)
            assert len(tiles) == 20
            tile = service.get_tile_mapping(s.style_id, 1)
            assert tile is not None and tile.tile_code == 1
            props = service.get_props_for_style(s.style_id)
            assert len(props) >= 3
            return True

        with concurrent.futures.ThreadPoolExecutor(max_workers=50) as pool:
            futures = [pool.submit(reader_task, i) for i in range(250)]
            results = [f.result() for f in concurrent.futures.as_completed(futures)]

        assert len(results) == 250
        assert all(r is True for r in results)

    def test_concurrent_read_during_force_reseed(self, isolated_temp_db: str) -> None:
        """Concurrent readers querying database while writers execute seed_database(force=True)."""
        mut_service = MapStyleService(db_path=isolated_temp_db)

        def writer_task() -> bool:
            for _ in range(3):
                cnt = mut_service.seed_database(force=True)
                assert cnt == 30
            return True

        def reader_task(idx: int) -> bool:
            for _ in range(15):
                style = mut_service.get_style_by_code((idx % 30) + 1)
                assert style is not None
                tiles = mut_service.get_tiles_for_style(style.style_id)
                assert len(tiles) == 20
            return True

        with concurrent.futures.ThreadPoolExecutor(max_workers=20) as pool:
            write_futures = [pool.submit(writer_task) for _ in range(4)]
            read_futures = [pool.submit(reader_task, i) for i in range(16)]
            all_futures = write_futures + read_futures
            results = [f.result() for f in concurrent.futures.as_completed(all_futures)]

        assert len(results) == 20
        assert all(r is True for r in results)

    def test_in_memory_service_thread_boundary(self) -> None:
        """In-memory SQLite connection is bound to creation thread unless configured."""
        mem_service = MapStyleService(db_path=":memory:")
        assert mem_service.get_style_by_code(1) is not None

        def cross_thread_call() -> None:
            mem_service.get_style_by_code(1)

        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
            future = pool.submit(cross_thread_call)
            with pytest.raises(sqlite3.ProgrammingError) as exc_info:
                future.result()
            assert "SQLite objects created in a thread can only be used in that same thread" in str(exc_info.value)
        mem_service.close()


class TestMapStyleEdgeCasesAndBoundaries:
    """Stress-test out-of-range boundaries, invalid types, and malformed inputs."""

    @pytest.mark.parametrize("invalid_code", [0, -1, -999, 31, 32, 100, 255, 256, 999, 2**31 - 1, -2**31])
    def test_out_of_range_biome_codes_return_none(self, service: MapStyleService, invalid_code: int) -> None:
        """All out-of-range integer biome codes return None without crashing."""
        assert service.get_style_by_code(invalid_code) is None

    @pytest.mark.parametrize("invalid_tile", [-1, -10, -999, 20, 21, 50, 100, 999, 2**31 - 1])
    def test_out_of_range_tile_codes_return_none(self, service: MapStyleService, invalid_tile: int) -> None:
        """All out-of-range integer tile codes return None without crashing."""
        mapping = service.get_tile_mapping("STY_01_HOANG_MANG_CO_LO", invalid_tile)
        assert mapping is None

    @pytest.mark.parametrize("type_mismatch", ["1", 1.0, 1.5, None, [], {}, object()])
    def test_biome_code_type_mismatches_handled_safely(self, service: MapStyleService, type_mismatch: object) -> None:
        """Non-integer biome code inputs either return None or raise standard Python errors gracefully."""
        try:
            res = service.get_style_by_code(type_mismatch)  # type: ignore
            # If SQLite accepts string '1' by coercion or returns None, it must not return corrupted data
            if res is not None:
                assert isinstance(res, MapStyleDefinition)
        except (sqlite3.InterfaceError, sqlite3.ProgrammingError, TypeError):
            pass

    @pytest.mark.parametrize("bad_style_id", ["", "   ", "\t\n", "sty_01_lowercase", "UNKNOWN_NONEXISTENT"])
    def test_invalid_style_ids_return_empty_or_none(self, service: MapStyleService, bad_style_id: str) -> None:
        """Invalid, empty, or uncanonical style_ids return None or empty collections."""
        assert service.get_style(bad_style_id) is None
        assert service.get_tiles_for_style(bad_style_id) == []
        assert service.get_tile_mapping(bad_style_id, 1) is None
        assert service.get_props_for_style(bad_style_id) == []


class TestMapStyleSQLInjectionImmunity:
    """Stress-test SQL injection attacks across all service query methods."""

    SQLI_PAYLOADS = [
        "' OR '1'='1",
        "'; DROP TABLE map_styles; --",
        "'; DROP TABLE map_style_tiles; --",
        "'; DROP TABLE map_style_props; --",
        "' UNION SELECT * FROM map_styles --",
        "STY_01_HOANG_MANG_CO_LO' OR '1'='1",
        "admin'--",
        "' OR 1=1; --",
        "SLEEP(5)",
        "'; VACUUM; --",
        "STY_01' /* comment */",
        "<script>alert('xss')</script>",
        "' OR biome_code=1 --",
        "%",
        "_",
        "' AND (SELECT 1 FROM sqlite_master) --",
        "STY_01\x00_INJECTION",
        "A" * 10000,  # 10KB buffer stress
        "🐉_LORE_STYLE_' OR 1=1;--",
    ]

    @pytest.mark.parametrize("payload", SQLI_PAYLOADS)
    def test_sqli_payload_resistance(self, service: MapStyleService, payload: str) -> None:
        """Verify queries with SQL injection payloads safely return None/empty and preserve DB."""
        # Query single style
        res = service.get_style(payload)
        assert res is None

        # Query tiles
        tiles = service.get_tiles_for_style(payload)
        assert tiles == []

        # Query single tile mapping
        tile = service.get_tile_mapping(payload, 1)
        assert tile is None

        # Query props
        props = service.get_props_for_style(payload)
        assert props == []

    def test_database_integrity_after_sqli_barrage(self, service: MapStyleService) -> None:
        """Ensure map_styles table remains exactly 30 records after injection attempts."""
        styles = service.list_styles()
        assert len(styles) == 30
        assert [s.biome_code for s in styles] == list(range(1, 31))


class TestMapStyleRecoveryAndSchemaIntegrity:
    """Test auto-creation, recovery from partial/empty DBs, and CHECK constraints."""

    def test_missing_database_file_and_dir_auto_recovery(self) -> None:
        """Service automatically creates missing directory, database file, and seeds 30 styles."""
        tmpdir = tempfile.mkdtemp(prefix="fe_recovery_")
        try:
            missing_db = os.path.join(tmpdir, "nested", "deep", "map_styles.db")
            assert not os.path.exists(missing_db)

            svc = MapStyleService(db_path=missing_db)
            assert os.path.isfile(missing_db)
            assert len(svc.list_styles()) == 30
            assert len(svc.get_tiles_for_style("STY_01_HOANG_MANG_CO_LO")) == 20
        finally:
            shutil.rmtree(tmpdir, ignore_errors=True)

    def test_zero_byte_empty_file_auto_recovery(self) -> None:
        """Service recovers from a pre-existing 0-byte file by initializing DDL and seeding."""
        tmpdir = tempfile.mkdtemp(prefix="fe_empty_")
        try:
            empty_db = os.path.join(tmpdir, "empty.db")
            with open(empty_db, "wb") as f:
                pass
            assert os.path.getsize(empty_db) == 0

            svc = MapStyleService(db_path=empty_db)
            assert os.path.getsize(empty_db) > 0
            assert len(svc.list_styles()) == 30
        finally:
            shutil.rmtree(tmpdir, ignore_errors=True)

    def test_partial_database_auto_reseed_recovery(self) -> None:
        """If database has fewer than 30 styles, MapStyleService re-seeds to 30."""
        tmpdir = tempfile.mkdtemp(prefix="fe_partial_")
        try:
            partial_db = os.path.join(tmpdir, "partial.db")
            svc = MapStyleService(db_path=partial_db)
            # Delete records to create partial database state
            with svc._get_connection() as conn:
                conn.execute("DELETE FROM map_styles WHERE biome_code > 5")
                conn.commit()

            # Verify only 5 styles exist
            with svc._get_connection() as conn:
                cur = conn.cursor()
                cur.execute("SELECT count(*) FROM map_styles")
                assert cur.fetchone()[0] == 5

            # Re-instantiating service should detect cnt < 30 and re-seed
            recovered_svc = MapStyleService(db_path=partial_db)
            assert len(recovered_svc.list_styles()) == 30
        finally:
            shutil.rmtree(tmpdir, ignore_errors=True)

    def test_corrupted_database_file_raises_database_error(self) -> None:
        """Garbage-filled database file cleanly raises sqlite3.DatabaseError."""
        tmpdir = tempfile.mkdtemp(prefix="fe_corrupt_")
        try:
            corrupt_db = os.path.join(tmpdir, "corrupt.db")
            with open(corrupt_db, "wb") as f:
                f.write(b"CORRUPTED_NON_SQLITE_GARBAGE_BYTES_1234567890")

            with pytest.raises(sqlite3.DatabaseError):
                MapStyleService(db_path=corrupt_db)
        finally:
            shutil.rmtree(tmpdir, ignore_errors=True)

    @pytest.mark.parametrize("invalid_biome_code", [0, 256])
    def test_schema_check_constraint_biome_code_range(self, isolated_temp_db: str, invalid_biome_code: int) -> None:
        """CHECK (biome_code BETWEEN 1 AND 255) rejects invalid inserts."""
        conn = init_db(isolated_temp_db)
        cur = conn.cursor()
        with pytest.raises(sqlite3.IntegrityError):
            cur.execute(
                """
                INSERT INTO map_styles (
                    style_id, biome_code, name_vi, name_en, theme_category,
                    theme_category_name, description, floor_color_hex, wall_color_hex,
                    path_color_hex, liquid_color_hex, ambient_light_hex, asset_dir, monster_family
                ) VALUES (?, ?, 'Vi', 'En', 'HOANG_DA_DA_NGOAI', 'T', 'D', '#111', '#222', '#333', '#444', '#555', 'a', 'm')
                """,
                (f"STY_TEST_{invalid_biome_code}", invalid_biome_code),
            )
        conn.close()

    @pytest.mark.parametrize("invalid_tile_type_id", [-1, 20])
    def test_schema_check_constraint_tile_type_id_range(self, isolated_temp_db: str, invalid_tile_type_id: int) -> None:
        """CHECK (tile_type_id BETWEEN 0 AND 19) rejects invalid tile IDs."""
        svc = MapStyleService(db_path=isolated_temp_db)
        with svc._get_connection() as conn:
            cur = conn.cursor()
            with pytest.raises(sqlite3.IntegrityError):
                cur.execute(
                    """
                    INSERT INTO map_style_tiles (
                        style_id, tile_type_id, tile_type_name, texture_filename,
                        normal_filename, base_color_fallback
                    ) VALUES ('STY_01_HOANG_MANG_CO_LO', ?, 'INVALID', 't.png', 'n.png', '#111111')
                    """,
                    (invalid_tile_type_id,),
                )
