"""Automated tests for registration uniqueness validation.

Run with:  python test_registration_validation.py
All tests must pass before deployment.
"""

import io
import json
import os
import sqlite3
import sys
import tempfile
import unittest
from pathlib import Path

# Ensure project root is importable
sys.path.insert(0, str(Path(__file__).resolve().parent))

import app as flask_app
from app import (
    normalize_cnic,
    normalize_email,
    validate_registration_constraints,
    parse_team_members,
    db,
    init_db,
)


# ---------------------------------------------------------------------------
# Helper: create an in-memory DB seeded with one game
# ---------------------------------------------------------------------------
def _seed_game(conn, game_id=1, title="PUBG Mobile", min_m=1, max_m=4):
    conn.execute(
        """INSERT OR REPLACE INTO games
           (id, title, platform, individual_price, group_price,
            active, created_at, min_members, max_members)
           VALUES (?, ?, 'Mobile', 'Rs.600', 'Rs.2000', 1, '2026-01-01T00:00', ?, ?)""",
        (game_id, title, min_m, max_m),
    )
    conn.commit()


def _seed_game2(conn, game_id=2, title="Tekken 8"):
    conn.execute(
        """INSERT OR REPLACE INTO games
           (id, title, platform, individual_price, group_price,
            active, created_at, min_members, max_members)
           VALUES (?, ?, 'PC', 'Rs.700', 'Rs.2400', 1, '2026-01-01T00:00', 1, 1)""",
        (game_id, title),
    )
    conn.commit()


_reg_counter = 0

def _insert_registration(conn, **kw):
    """Insert a registration row with sane defaults and return its id."""
    global _reg_counter
    _reg_counter += 1
    token = f"tok_{_reg_counter}_{os.urandom(4).hex()}"
    serial = f"NX-TEST-{_reg_counter:06d}"
    defaults = {
        "full_name": "Test User",
        "email": "test@example.com",
        "phone": "03001234567",
        "cnic": "35202-1234567-1",
        "institution_type": "University",
        "university": "FAST",
        "roll_no": "",
        "entry_type": "individual",
        "game_id": 1,
        "team_name": "TestNick",
        "leader_name": "",
        "members": "",
        "payment_method": "G-Pay",
        "payment_status": "under_verification",
        "proof_image": "",
        "ticket_token": token,
        "ticket_serial": serial,
        "created_at": "2026-08-01T00:00:00",
    }
    defaults.update(kw)
    cols = ", ".join(defaults.keys())
    phs = ", ".join("?" for _ in defaults)
    cur = conn.execute(
        f"INSERT INTO registrations ({cols}) VALUES ({phs})",
        list(defaults.values()),
    )
    conn.commit()
    return cur.lastrowid


# ===========================================================================
class TestNormalize(unittest.TestCase):
    def test_email_lowercase_and_trim(self):
        self.assertEqual(normalize_email("  Test@Example.COM  "), "test@example.com")

    def test_email_none(self):
        self.assertEqual(normalize_email(None), "")

    def test_cnic_strips_hyphens_spaces_dots(self):
        self.assertEqual(normalize_cnic("35202-1234567-1"), "3520212345671")
        self.assertEqual(normalize_cnic("35202 1234567 1"), "3520212345671")
        self.assertEqual(normalize_cnic("35202.1234567.1"), "3520212345671")
        self.assertEqual(normalize_cnic("  35202 - 1234567 - 1  "), "3520212345671")

    def test_cnic_none(self):
        self.assertEqual(normalize_cnic(None), "")


# ===========================================================================
class TestNicknamePerGame(unittest.TestCase):
    """Rule: Nickname / team name must be unique within the same game.
    Same nickname in a different game IS allowed."""

    def setUp(self):
        self.conn = db()
        _seed_game(self.conn)
        _seed_game2(self.conn, game_id=2)

    def tearDown(self):
        self.conn.execute("DELETE FROM registrations")
        self.conn.commit()
        self.conn.close()

    def test_same_nickname_same_game_blocked(self):
        _insert_registration(self.conn, team_name="Alpha", game_id=1)
        errors = validate_registration_constraints(
            self.conn, "b@x.com", "11111-1111111-1", 1, "Alpha", "individual"
        )
        self.assertTrue(any("Nickname is already registered for this game" in e for e in errors))

    def test_same_nickname_different_game_allowed(self):
        _insert_registration(self.conn, team_name="SniperX", game_id=1, entry_type="individual")
        errors = validate_registration_constraints(
            self.conn, "f@x.com", "55555-5555555-5", 2, "SniperX", "individual"
        )
        nick_errors = [e for e in errors if "Nickname" in e]
        self.assertFalse(nick_errors)

    def test_case_insensitive_nickname_same_game_blocked(self):
        _insert_registration(self.conn, team_name="Alpha", game_id=1)
        errors = validate_registration_constraints(
            self.conn, "d@x.com", "33333-3333333-3", 1, "alpha", "group"
        )
        self.assertTrue(any("Nickname is already registered for this game" in e for e in errors))

    def test_different_nicknames_same_game_allowed(self):
        _insert_registration(self.conn, team_name="Alpha", game_id=1)
        errors = validate_registration_constraints(
            self.conn, "e@x.com", "44444-4444444-4", 1, "Bravo", "individual"
        )
        self.assertFalse(errors)

    def test_same_team_name_different_game_blocked_by_index(self):
        """DB index allows same nickname in different game — no IntegrityError."""
        _insert_registration(self.conn, team_name="Alpha", game_id=1)
        rid = _insert_registration(self.conn, team_name="Alpha", game_id=2,
                                   email="other@x.com", cnic="99999-9999999-9")
        self.assertIsNotNone(rid)


# ===========================================================================
class TestCnicPerGame(unittest.TestCase):
    """Rule: CNIC must be unique within the same game."""

    def setUp(self):
        self.conn = db()
        _seed_game(self.conn)
        _seed_game2(self.conn, game_id=2)

    def tearDown(self):
        self.conn.execute("DELETE FROM registrations")
        self.conn.commit()
        self.conn.close()

    def test_same_cnic_same_game_blocked(self):
        _insert_registration(self.conn, email="a@test.com", cnic="11111-1111111-1", game_id=1)
        errors = validate_registration_constraints(
            self.conn, "a@test.com", "11111-1111111-1", 1, "Nick2", "individual"
        )
        self.assertTrue(any("CNIC is already registered for this game" in e for e in errors))

    def test_same_cnic_different_game_allowed(self):
        _insert_registration(self.conn, email="a@test.com", cnic="11111-1111111-1", game_id=1)
        errors = validate_registration_constraints(
            self.conn, "b@test.com", "11111-1111111-1", 2, "Nick5b", "individual"
        )
        cnic_errors = [e for e in errors if "CNIC is already registered for this game" in e]
        self.assertFalse(cnic_errors)

    def test_different_email_same_cnic_same_game_blocked(self):
        _insert_registration(self.conn, email="a@test.com", cnic="11111-1111111-1", game_id=1)
        errors = validate_registration_constraints(
            self.conn, "b@test.com", "11111-1111111-1", 1, "Nick5", "individual"
        )
        self.assertTrue(any("CNIC is already registered for this game" in e for e in errors))

    def test_same_email_different_cnic_allowed(self):
        _insert_registration(self.conn, email="a@test.com", cnic="11111-1111111-1", game_id=1)
        errors = validate_registration_constraints(
            self.conn, "a@test.com", "22222-2222222-2", 1, "Nick4", "individual"
        )
        cnic_errors = [e for e in errors if "CNIC" in e]
        self.assertFalse(cnic_errors)

    def test_cnic_formatting_bypass_prevented(self):
        _insert_registration(self.conn, email="x@y.com", cnic="35202-1234567-1", game_id=1)
        errors = validate_registration_constraints(
            self.conn, "x@y.com", "35202 1234567 1", 1, "Nick7", "individual"
        )
        self.assertTrue(any("CNIC is already registered for this game" in e for e in errors))

    def test_cnic_dots_bypass_prevented(self):
        _insert_registration(self.conn, email="x@y.com", cnic="35202-1234567-1", game_id=1)
        errors = validate_registration_constraints(
            self.conn, "x@y.com", "35202.1234567.1", 1, "Nick8", "individual"
        )
        self.assertTrue(any("CNIC is already registered for this game" in e for e in errors))

    def test_cnic_only_formatting_bypass_prevented(self):
        _insert_registration(self.conn, email="a@test.com", cnic="35202-1234567-1", game_id=1)
        errors = validate_registration_constraints(
            self.conn, "other@test.com", "35202 1234567 1", 1, "Nick9", "individual"
        )
        self.assertTrue(any("CNIC is already registered for this game" in e for e in errors))

    def test_exclude_id_allows_self_update(self):
        rid = _insert_registration(self.conn, email="a@test.com", cnic="11111-1111111-1", game_id=1)
        errors = validate_registration_constraints(
            self.conn, "a@test.com", "11111-1111111-1", 1, "SameNick", "individual",
            exclude_id=rid,
        )
        cnic_errors = [e for e in errors if "CNIC" in e]
        self.assertFalse(cnic_errors)


# ===========================================================================
class TestTeamMemberDuplicate(unittest.TestCase):
    """Rule: A player (email) already registered for this game cannot register again."""

    def setUp(self):
        self.conn = db()
        _seed_game(self.conn, min_m=2, max_m=4)

    def tearDown(self):
        self.conn.execute("DELETE FROM registrations")
        self.conn.commit()
        self.conn.close()

    def test_existing_member_trying_to_register_as_primary_blocked(self):
        """Member of Team A tries to register individually for the same game."""
        _insert_registration(
            self.conn,
            email="leader@team.com",
            cnic="11111-1111111-1",
            game_id=1,
            team_name="TeamA",
            entry_type="group",
            members="Member 1: Bob | bob@team.com | 03001111111",
        )
        errors = validate_registration_constraints(
            self.conn, "bob@team.com", "22222-2222222-2", 1, "SoloNick", "individual"
        )
        self.assertTrue(any("already registered in this game with another team" in e for e in errors))

    def test_new_team_with_already_registered_member_blocked(self):
        """Team B tries to add someone who is already in Team A for the same game."""
        _insert_registration(
            self.conn,
            email="leader@team.com",
            cnic="11111-1111111-1",
            game_id=1,
            team_name="TeamA",
            entry_type="group",
            members="Member 1: Bob | bob@team.com | 03001111111",
        )
        new_members = "Member 1: Bob | bob@team.com | 03001111111"
        errors = validate_registration_constraints(
            self.conn, "other@team.com", "33333-3333333-3", 1, "TeamB", "group",
            members_text=new_members,
        )
        self.assertTrue(any("already registered in this game with another team" in e for e in errors))

    def test_member_in_different_game_allowed(self):
        """Same person can be in a team for a different game."""
        _seed_game(self.conn, game_id=2, title="FIFA")
        _insert_registration(
            self.conn,
            email="leader@team.com",
            cnic="11111-1111111-1",
            game_id=1,
            team_name="TeamA",
            entry_type="group",
            members="Member 1: Bob | bob@team.com | 03001111111",
        )
        errors = validate_registration_constraints(
            self.conn, "other@team.com", "44444-4444444-4", 2, "TeamB_FIFA", "group",
            members_text="Member 1: Bob | bob@team.com | 03001111111",
        )
        member_errors = [e for e in errors if "already registered in this game with another team" in e]
        self.assertFalse(member_errors)

    def test_member_joining_as_email_in_another_members_list_blocked(self):
        """Person X's email appears in the primary registrant field of a different team for the same game."""
        _insert_registration(
            self.conn,
            email="alice@test.com",
            cnic="11111-1111111-1",
            game_id=1,
            team_name="TeamA",
            entry_type="group",
            members="Member 1: Bob | bob@test.com | 03001111111",
        )
        new_members = "Member 1: Alice | alice@test.com | 03002222222"
        errors = validate_registration_constraints(
            self.conn, "carol@test.com", "33333-3333333-3", 1, "TeamC", "group",
            members_text=new_members,
        )
        self.assertTrue(any("already registered in this game with another team" in e for e in errors))

    def test_exclude_id_allows_self_update(self):
        """Admin editing the same registration should not trigger member duplicate."""
        rid = _insert_registration(
            self.conn,
            email="leader@team.com",
            cnic="11111-1111111-1",
            game_id=1,
            team_name="TeamA",
            entry_type="group",
            members="Member 1: Bob | bob@team.com | 03001111111",
        )
        errors = validate_registration_constraints(
            self.conn, "leader@team.com", "11111-1111111-1", 1, "TeamA", "group",
            members_text="Member 1: Bob | bob@team.com | 03001111111",
            exclude_id=rid,
        )
        self.assertFalse(errors)


# ===========================================================================
class TestFullNamesAllowed(unittest.TestCase):
    """Rule: Same full name across registrations IS allowed."""

    def setUp(self):
        self.conn = db()
        _seed_game(self.conn)

    def tearDown(self):
        self.conn.execute("DELETE FROM registrations")
        self.conn.commit()
        self.conn.close()

    def test_same_full_name_different_email_allowed(self):
        _insert_registration(self.conn, full_name="Ahmed Khan", email="a@x.com", cnic="11111-1111111-1", team_name="Nick1")
        errors = validate_registration_constraints(
            self.conn, "b@x.com", "22222-2222222-2", 1, "Nick2", "individual"
        )
        self.assertFalse(errors)


# ===========================================================================
class TestDatabaseConstraints(unittest.TestCase):
    """Verify the DB-level unique indexes fire on duplicates."""

    def setUp(self):
        self.conn = db()
        _seed_game(self.conn)
        _seed_game2(self.conn, game_id=2)

    def tearDown(self):
        self.conn.execute("DELETE FROM registrations")
        self.conn.commit()
        self.conn.close()

    def test_duplicate_team_name_same_game_raises_integrity_error(self):
        _insert_registration(self.conn, team_name="UniqueTeam", game_id=1)
        with self.assertRaises(sqlite3.IntegrityError):
            _insert_registration(self.conn, team_name="UniqueTeam", game_id=1,
                                 email="other@x.com", cnic="99999-9999999-9")

    def test_same_team_name_different_game_allowed(self):
        """Per-game index allows same team name in different game."""
        _insert_registration(self.conn, team_name="Alpha", game_id=1)
        rid = _insert_registration(self.conn, team_name="Alpha", game_id=2,
                                   email="other@x.com", cnic="99999-9999999-9")
        self.assertIsNotNone(rid)

    def test_duplicate_email_cnic_game_raises_integrity_error(self):
        _insert_registration(self.conn, email="dup@test.com", cnic="11111-1111111-1", game_id=1, team_name="NickA")
        with self.assertRaises(sqlite3.IntegrityError):
            _insert_registration(self.conn, email="dup@test.com", cnic="11111-1111111-1",
                                 game_id=1, team_name="NickB")

    def test_same_email_cnic_different_game_allowed(self):
        _insert_registration(self.conn, email="dup@test.com", cnic="11111-1111111-1", game_id=1, team_name="NickA")
        _insert_registration(self.conn, email="dup@test.com", cnic="11111-1111111-1",
                             game_id=2, team_name="NickB")


# ===========================================================================
class TestIntegrationFlaskClient(unittest.TestCase):
    """Test the /register POST endpoint through Flask test client."""

    def setUp(self):
        flask_app.app.config["TESTING"] = True
        flask_app.app.config["WTF_CSRF_ENABLED"] = False
        self.client = flask_app.app.test_client()

        with db() as conn:
            _seed_game(conn)
            _seed_game2(conn, game_id=2)
            conn.execute("DELETE FROM registrations")
            conn.commit()

        import struct, zlib
        def _make_png():
            raw = b"\x00\xff\x00\x00"
            compressed = zlib.compress(raw)
            def _chunk(ctype, data):
                c = ctype + data
                return struct.pack(">I", len(data)) + c + struct.pack(">I", zlib.crc32(c) & 0xffffffff)
            return b"\x89PNG\r\n\x1a\n" + _chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0)) + _chunk(b"IDAT", compressed) + _chunk(b"IEND", b"")
        self.png_data = _make_png()

    def tearDown(self):
        with db() as conn:
            conn.execute("DELETE FROM registrations")
            conn.commit()

    def _post_registration(self, **overrides):
        data = {
            "full_name": "Test User",
            "email": "test@example.com",
            "phone": "03001234567",
            "cnic": "35202-1234567-1",
            "institution_type": "University",
            "university": "FAST",
            "roll_no": "",
            "game_id": "1",
            "team_name": "MyTeam",
            "team_size": "1",
            "leader_name": "",
            "proof_image": (io.BytesIO(self.png_data), "test.png"),
        }
        data.update(overrides)
        return self.client.post(
            "/register",
            data=data,
            content_type="multipart/form-data",
            follow_redirects=True,
        )

    def test_individual_registration_succeeds(self):
        resp = self._post_registration()
        self.assertEqual(resp.status_code, 200)
        self.assertIn(b"submitted", resp.data.lower())

    def test_duplicate_nickname_same_game_rejected(self):
        self._post_registration(team_name="Alpha")
        resp = self._post_registration(email="other@x.com", cnic="99999-9999999-9", team_name="Alpha")
        self.assertIn(b"Nickname is already registered for this game", resp.data)

    def test_same_nickname_different_game_allowed(self):
        self._post_registration(game_id="1", team_name="SharedNick")
        resp = self._post_registration(game_id="2", team_name="SharedNick",
                                        email="other@x.com", cnic="99999-9999999-9")
        self.assertIn(b"submitted", resp.data.lower())

    def test_duplicate_cnic_same_game_rejected(self):
        self._post_registration()
        resp = self._post_registration(team_name="Other")
        self.assertIn(b"CNIC is already registered for this game", resp.data)

    def test_same_email_cnic_different_game_allowed(self):
        self._post_registration(game_id="1", team_name="Nick1")
        resp = self._post_registration(game_id="2", team_name="Nick2")
        self.assertIn(b"submitted", resp.data.lower())

    def test_nickname_case_insensitive_rejected(self):
        self._post_registration(team_name="Phoenix")
        resp = self._post_registration(email="other@x.com", cnic="99999-9999999-9", team_name="phoenix")
        self.assertIn(b"Nickname is already registered for this game", resp.data)


class TestImagePathHandling(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.old_static = flask_app.STATIC_DIR
        self.old_upload = flask_app.UPLOAD_DIR
        self.old_image_root = flask_app.IMAGE_ROOT
        flask_app.STATIC_DIR = Path(self.tmp.name) / "static"
        flask_app.UPLOAD_DIR = flask_app.STATIC_DIR / "uploads"
        flask_app.IMAGE_ROOT = flask_app.STATIC_DIR / "Images"
        (flask_app.IMAGE_ROOT / "Our Team").mkdir(parents=True)
        (flask_app.IMAGE_ROOT / "Logo").mkdir(parents=True)
        (flask_app.IMAGE_ROOT / "Registrations" / "PUBG Mobile").mkdir(parents=True)
        flask_app.UPLOAD_DIR.mkdir(parents=True)
        (flask_app.IMAGE_ROOT / "Our Team" / "Member One.WebP").write_bytes(b"webp")
        (flask_app.IMAGE_ROOT / "Logo" / "Nexora Logo.webp").write_bytes(b"webp")
        (flask_app.IMAGE_ROOT / "Registrations" / "PUBG Mobile" / "proof.webp").write_bytes(b"secret")
        flask_app.invalidate_image_cache()
        flask_app.app.config["TESTING"] = True
        self.client = flask_app.app.test_client()

    def tearDown(self):
        flask_app.STATIC_DIR = self.old_static
        flask_app.UPLOAD_DIR = self.old_upload
        flask_app.IMAGE_ROOT = self.old_image_root
        flask_app.invalidate_image_cache()
        self.tmp.cleanup()

    def test_windows_static_prefix_spaces_and_extension_case_resolve(self):
        rel = flask_app.img_static_url(r"static\Images\Our Team\Member One.webp")
        self.assertEqual(rel, "Images/Our Team/Member One.WebP")
        self.assertTrue(flask_app.static_file_abs("/static/Images/Our Team/Member One.webp").is_file())

    def test_bare_filename_uses_uploads_legacy_fallback(self):
        (flask_app.UPLOAD_DIR / "legacy.webp").write_bytes(b"webp")
        flask_app.invalidate_image_cache()
        self.assertEqual(flask_app.img_static_url("legacy.png"), "uploads/legacy.webp")

    def test_directory_traversal_is_rejected(self):
        self.assertEqual(flask_app.img_static_url("../nexora.db"), "")
        self.assertIsNone(flask_app.static_file_abs("../nexora.db"))

    def test_public_media_route_blocks_registration_proofs(self):
        resp = self.client.get("/media/Images/Registrations/PUBG%20Mobile/proof.webp")
        self.assertEqual(resp.status_code, 404)


# ===========================================================================
if __name__ == "__main__":
    unittest.main(verbosity=2)
