#!/usr/bin/env python3
"""
Automated tests for the token/payment system.

Run with: python test_token_system.py
Or with pytest: pytest test_token_system.py -v
"""

import os
import sys
import tempfile
import unittest
from pathlib import Path

# Set test database path before importing modules
TEST_DB_PATH = tempfile.mktemp(suffix=".db")
os.environ["DB_PATH"] = TEST_DB_PATH
os.environ["BUILD_SOURCE"] = "test"

from database import (
    init_db, get_db_path, get_connection,
    get_user, get_or_create_user, update_user,
    get_all_users, get_users_count, search_users,
    create_transaction, get_transaction, get_user_transactions,
    get_recent_transactions, get_transaction_by_task,
    get_promo_remaining, set_promo_remaining, set_user_promo_eligible,
    get_stats,
    # Giveaway functions
    get_giveaway_config, is_giveaway_active, get_giveaway_remaining,
    start_giveaway, stop_giveaway,
    # Gift pack functions
    create_gift_pack, get_user_active_packs, redeem_gift_pack,
    expire_gift_packs, get_expiring_packs, mark_pack_notified,
    get_user_packs, get_gift_pack_stats, has_received_gift_pack,
    set_gift_pack_received, create_gift_pack_campaign,
    get_active_campaigns, get_matching_campaign, deactivate_campaign,
    decrement_campaign_slots, get_all_campaigns, get_inactive_users,
)

from token_manager import (
    get_balance, has_tokens, is_blocked, is_promo_eligible,
    deduct_token, refund_token, credit_tokens, revoke_tokens,
    block_user, unblock_user, get_user_info, migrate_user,
    check_and_grant_giveaway, check_and_grant_gift_pack,
    check_and_redeem_gift_pack
)

from survey_db import (
    create_survey, get_survey, get_active_surveys, deactivate_survey,
    create_invitation, get_invitation, get_invitation_by_id,
    update_invitation_status, mark_invitation_rewarded,
    save_response, get_user_responses,
    get_survey_target_users, get_survey_stats, get_all_responses_export,
    DEFAULT_QUESTIONS,
)


class TestDatabase(unittest.TestCase):
    """Tests for database.py"""

    @classmethod
    def setUpClass(cls):
        """Initialize test database."""
        init_db(Path(TEST_DB_PATH))

    def test_get_db_path(self):
        """Test database path resolution."""
        path = get_db_path()
        self.assertEqual(str(path), TEST_DB_PATH)

    def test_create_user(self):
        """Test user creation."""
        user = get_or_create_user(1001, "testuser1", "Test", "User")
        self.assertEqual(user["user_id"], 1001)
        self.assertEqual(user["username"], "testuser1")
        self.assertEqual(user["first_name"], "Test")
        self.assertEqual(user["balance"], 0)

    def test_get_existing_user(self):
        """Test getting existing user."""
        # Create user
        get_or_create_user(1002, "testuser2", "Test2")
        # Get same user
        user = get_or_create_user(1002)
        self.assertEqual(user["user_id"], 1002)
        self.assertEqual(user["username"], "testuser2")

    def test_update_user(self):
        """Test user update."""
        get_or_create_user(1003, "testuser3", "Test3")
        result = update_user(1003, {"balance": 100})
        self.assertTrue(result)

        user = get_user(1003)
        self.assertEqual(user["balance"], 100)

    def test_get_nonexistent_user(self):
        """Test getting non-existent user."""
        user = get_user(999999)
        self.assertIsNone(user)

    def test_search_users_by_id(self):
        """Test user search by ID."""
        get_or_create_user(1004, "searchtest", "Search")
        results = search_users("1004")
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0]["user_id"], 1004)

    def test_search_users_by_username(self):
        """Test user search by username."""
        get_or_create_user(1005, "uniquename123", "Unique")
        results = search_users("uniquename")
        self.assertTrue(any(u["user_id"] == 1005 for u in results))

    def test_create_transaction(self):
        """Test transaction creation."""
        get_or_create_user(1006)
        txn_id = create_transaction(
            user_id=1006,
            amount=10,
            balance_before=0,
            balance_after=10,
            txn_type="admin_grant",
            description="Test grant"
        )
        self.assertIsNotNone(txn_id)
        self.assertGreater(txn_id, 0)

    def test_get_transaction(self):
        """Test getting transaction by ID."""
        get_or_create_user(1007)
        txn_id = create_transaction(
            user_id=1007,
            amount=5,
            balance_before=0,
            balance_after=5,
            txn_type="admin_grant"
        )
        txn = get_transaction(txn_id)
        self.assertIsNotNone(txn)
        self.assertEqual(txn["amount"], 5)
        self.assertEqual(txn["user_id"], 1007)

    def test_get_user_transactions(self):
        """Test getting user's transactions."""
        get_or_create_user(1008)
        create_transaction(1008, 1, 0, 1, "admin_grant")
        create_transaction(1008, 2, 1, 3, "admin_grant")
        create_transaction(1008, -1, 3, 2, "generation")

        txns = get_user_transactions(1008)
        self.assertEqual(len(txns), 3)
        # Should be ordered by created_at DESC
        self.assertEqual(txns[0]["amount"], -1)

    def test_transaction_by_task(self):
        """Test finding transaction by task_id."""
        get_or_create_user(1009)
        create_transaction(
            user_id=1009,
            amount=-1,
            balance_before=10,
            balance_after=9,
            txn_type="generation",
            task_id="task_12345"
        )
        txn = get_transaction_by_task("task_12345")
        self.assertIsNotNone(txn)
        self.assertEqual(txn["task_id"], "task_12345")

    def test_promo_config(self):
        """Test promo configuration."""
        set_promo_remaining(50)
        remaining = get_promo_remaining()
        self.assertEqual(remaining, 50)

    def test_stats(self):
        """Test statistics gathering."""
        stats = get_stats()
        self.assertIn("total_users", stats)
        self.assertIn("total_balance", stats)
        self.assertIn("transactions", stats)


class TestTokenManager(unittest.TestCase):
    """Tests for token_manager.py"""

    @classmethod
    def setUpClass(cls):
        """Initialize test database."""
        init_db(Path(TEST_DB_PATH))

    def test_get_balance_new_user(self):
        """Test balance for non-existent user."""
        balance = get_balance(2001)
        self.assertEqual(balance, 0)

    def test_has_tokens_false(self):
        """Test has_tokens returns False for empty balance."""
        get_or_create_user(2002)
        self.assertFalse(has_tokens(2002))

    def test_credit_tokens(self):
        """Test crediting tokens."""
        get_or_create_user(2003)
        result = credit_tokens(2003, 10, "admin_grant", "Test credit")
        self.assertTrue(result)
        self.assertEqual(get_balance(2003), 10)

    def test_has_tokens_true(self):
        """Test has_tokens returns True after credit."""
        get_or_create_user(2004)
        credit_tokens(2004, 5, "admin_grant", "Test")
        self.assertTrue(has_tokens(2004, 1))
        self.assertTrue(has_tokens(2004, 5))
        self.assertFalse(has_tokens(2004, 6))

    def test_deduct_token(self):
        """Test token deduction."""
        get_or_create_user(2005)
        credit_tokens(2005, 10, "admin_grant", "Initial")

        txn_id = deduct_token(2005, "task_001", "product_001")
        self.assertIsNotNone(txn_id)
        self.assertEqual(get_balance(2005), 9)

    def test_deduct_token_insufficient(self):
        """Test deduction with insufficient balance."""
        get_or_create_user(2006)
        # No tokens credited
        txn_id = deduct_token(2006, "task_002", "product_002")
        self.assertIsNone(txn_id)

    def test_refund_token(self):
        """Test token refund."""
        get_or_create_user(2007)
        credit_tokens(2007, 10, "admin_grant", "Initial")

        # Deduct
        deduct_token(2007, "task_003", "product_003")
        self.assertEqual(get_balance(2007), 9)

        # Refund
        result = refund_token(2007, "task_003", "Test failure")
        self.assertTrue(result)
        self.assertEqual(get_balance(2007), 10)

    def test_refund_duplicate(self):
        """Test that duplicate refund fails."""
        get_or_create_user(2008)
        credit_tokens(2008, 10, "admin_grant", "Initial")
        deduct_token(2008, "task_004", "product_004")

        # First refund - should succeed
        result1 = refund_token(2008, "task_004", "First refund")
        self.assertTrue(result1)

        # Second refund - should fail
        result2 = refund_token(2008, "task_004", "Duplicate refund")
        self.assertFalse(result2)

        # Balance should still be 10 (not 11)
        self.assertEqual(get_balance(2008), 10)

    def test_revoke_tokens(self):
        """Test token revocation."""
        get_or_create_user(2009)
        credit_tokens(2009, 20, "admin_grant", "Initial")

        result = revoke_tokens(2009, 5, "Test revoke", initiated_by=1)
        self.assertTrue(result)
        self.assertEqual(get_balance(2009), 15)

    def test_revoke_insufficient(self):
        """Test revocation with insufficient balance."""
        get_or_create_user(2010)
        credit_tokens(2010, 5, "admin_grant", "Initial")

        result = revoke_tokens(2010, 10, "Too much", initiated_by=1)
        self.assertFalse(result)
        self.assertEqual(get_balance(2010), 5)

    def test_block_unblock_user(self):
        """Test blocking and unblocking user."""
        get_or_create_user(2011)

        self.assertFalse(is_blocked(2011))

        block_user(2011, "Test block", initiated_by=1)
        self.assertTrue(is_blocked(2011))

        unblock_user(2011)
        self.assertFalse(is_blocked(2011))

    def test_promo_eligibility(self):
        """Test promo eligibility."""
        # Set promo remaining to ensure new users get promo
        set_promo_remaining(100)

        user = get_or_create_user(2012)
        # New user should be promo eligible
        self.assertTrue(is_promo_eligible(2012))

    def test_migrate_user(self):
        """Test user migration with initial tokens."""
        result = migrate_user(2013, 1000, "migrated_user", "Migrated")
        self.assertTrue(result)
        self.assertEqual(get_balance(2013), 1000)

    def test_migrate_user_idempotent(self):
        """Test that migration is idempotent."""
        migrate_user(2014, 500)
        migrate_user(2014, 500)  # Second call should not add more
        self.assertEqual(get_balance(2014), 500)

    def test_get_user_info(self):
        """Test getting user info with transactions."""
        get_or_create_user(2015)
        credit_tokens(2015, 10, "admin_grant", "Test")
        deduct_token(2015, "task_info", "product_info")

        info = get_user_info(2015)
        self.assertIsNotNone(info)
        self.assertEqual(info["user"]["balance"], 9)
        self.assertGreaterEqual(len(info["transactions"]), 2)

    def test_transaction_balance_tracking(self):
        """Test that balance_before and balance_after are correct."""
        get_or_create_user(2016)

        credit_tokens(2016, 10, "admin_grant", "First")
        credit_tokens(2016, 5, "admin_grant", "Second")

        txns = get_user_transactions(2016)
        # Most recent first
        self.assertEqual(txns[0]["balance_before"], 10)
        self.assertEqual(txns[0]["balance_after"], 15)
        self.assertEqual(txns[1]["balance_before"], 0)
        self.assertEqual(txns[1]["balance_after"], 10)


class TestIntegration(unittest.TestCase):
    """Integration tests for full workflows."""

    @classmethod
    def setUpClass(cls):
        """Initialize test database."""
        init_db(Path(TEST_DB_PATH))

    def test_full_purchase_generation_flow(self):
        """Test complete flow: purchase -> generation -> success."""
        user_id = 3001
        get_or_create_user(user_id, "flow_user", "Flow")

        # Simulate purchase
        credit_tokens(
            user_id, 5, "purchase",
            "Purchase 5 tokens",
            telegram_payment_charge_id="tg_pay_001",
            provider_payment_charge_id="yoo_pay_001",
            amount_rub=249900
        )
        self.assertEqual(get_balance(user_id), 5)

        # Simulate generation
        txn_id = deduct_token(user_id, "gen_task_001", "wb_product_001")
        self.assertIsNotNone(txn_id)
        self.assertEqual(get_balance(user_id), 4)

        # Verify transaction details
        txn = get_transaction(txn_id)
        self.assertEqual(txn["type"], "generation")
        self.assertEqual(txn["task_id"], "gen_task_001")
        self.assertEqual(txn["product_id"], "wb_product_001")

    def test_full_generation_failure_refund_flow(self):
        """Test complete flow: generation -> failure -> refund."""
        user_id = 3002
        get_or_create_user(user_id)
        credit_tokens(user_id, 3, "admin_grant", "Test tokens")

        # Start generation
        deduct_token(user_id, "fail_task_001", "product_001")
        self.assertEqual(get_balance(user_id), 2)

        # Simulate failure and refund
        refund_token(user_id, "fail_task_001", "API error: timeout")
        self.assertEqual(get_balance(user_id), 3)

        # Verify refund transaction
        txns = get_user_transactions(user_id)
        refund_txn = next(t for t in txns if t["type"] == "refund")
        self.assertIn("API error", refund_txn["description"])

    def test_admin_grant_revoke_flow(self):
        """Test admin granting and revoking tokens."""
        user_id = 3003
        admin_id = 1  # Admin user

        get_or_create_user(user_id, "managed_user", "Managed")

        # Admin grants tokens
        credit_tokens(
            user_id, 100, "admin_grant",
            "Welcome bonus",
            initiated_by=admin_id
        )
        self.assertEqual(get_balance(user_id), 100)

        # Admin revokes some tokens
        revoke_tokens(user_id, 30, "Abuse detected", initiated_by=admin_id)
        self.assertEqual(get_balance(user_id), 70)

        # Verify audit trail
        txns = get_user_transactions(user_id)
        grant_txn = next(t for t in txns if t["type"] == "admin_grant")
        revoke_txn = next(t for t in txns if t["type"] == "admin_revoke")

        self.assertEqual(grant_txn["initiated_by"], admin_id)
        self.assertEqual(revoke_txn["initiated_by"], admin_id)


class TestPaymentHandlers(unittest.TestCase):
    """Tests for payment_handlers.py"""

    @classmethod
    def setUpClass(cls):
        """Initialize test database."""
        init_db(Path(TEST_DB_PATH))

    def test_format_price(self):
        """Test price formatting."""
        from payment_handlers import format_price
        self.assertEqual(format_price(69900), "699₽")
        self.assertEqual(format_price(9900), "99₽")
        self.assertEqual(format_price(100), "1₽")
        self.assertEqual(format_price(150), "1.50₽")

    def test_packages_configuration(self):
        """Test token packages are configured correctly."""
        from payment_handlers import PACKAGES

        # Check all packages exist
        self.assertIn("1", PACKAGES)
        self.assertIn("5", PACKAGES)
        self.assertIn("10", PACKAGES)

        # Check package structure
        for pkg_id, pkg in PACKAGES.items():
            self.assertIn("tokens", pkg)
            self.assertIn("regular", pkg)
            self.assertIn("promo", pkg)
            self.assertEqual(pkg["tokens"], int(pkg_id))
            self.assertGreater(pkg["regular"], pkg["promo"])

    def test_promo_pricing_discount(self):
        """Test promo prices are lower than regular."""
        from payment_handlers import PACKAGES

        for pkg_id, pkg in PACKAGES.items():
            discount_percent = (1 - pkg["promo"] / pkg["regular"]) * 100
            # Promo should be at least 50% off
            self.assertGreater(discount_percent, 50)

    def test_bulk_discount(self):
        """Test bulk packages have better per-token price."""
        from payment_handlers import PACKAGES

        # Calculate per-token price for each package (regular)
        per_token_1 = PACKAGES["1"]["regular"] / 1
        per_token_5 = PACKAGES["5"]["regular"] / 5
        per_token_10 = PACKAGES["10"]["regular"] / 10

        # Bulk should be cheaper per token
        self.assertGreater(per_token_1, per_token_5)
        self.assertGreater(per_token_5, per_token_10)

    def test_payment_flow_simulation(self):
        """Simulate complete payment flow without Telegram API."""
        from payment_handlers import PACKAGES

        user_id = 4001
        get_or_create_user(user_id, "payment_test", "Payment")

        # User is promo eligible (new user with promo remaining)
        set_promo_remaining(100)
        user = get_or_create_user(4002, "promo_test", "Promo")
        self.assertTrue(user["is_promo_eligible"])

        # Simulate selecting 5-token package
        pkg = PACKAGES["5"]
        tokens = pkg["tokens"]
        price = pkg["promo"]  # 49900 kopecks = 499 RUB

        # Simulate successful payment callback
        success = credit_tokens(
            user_id=4002,
            amount=tokens,
            txn_type="purchase",
            description=f"Purchase {tokens} tokens (promo)",
            telegram_payment_charge_id="tg_test_001",
            provider_payment_charge_id="yoo_test_001",
            amount_rub=price,
        )
        self.assertTrue(success)
        self.assertEqual(get_balance(4002), 5)

        # Verify transaction logged correctly
        info = get_user_info(4002)
        txn = info["transactions"][0]
        self.assertEqual(txn["type"], "purchase")
        self.assertEqual(txn["amount"], 5)
        self.assertEqual(txn["amount_rub"], 49900)
        self.assertEqual(txn["telegram_payment_charge_id"], "tg_test_001")

    def test_promo_eligibility_flow(self):
        """Test promo eligibility tracking."""
        # Set limited promo slots
        set_promo_remaining(2)

        # First user gets promo
        user1 = get_or_create_user(4010, "promo1", "Promo1")
        self.assertTrue(user1["is_promo_eligible"])
        self.assertEqual(get_promo_remaining(), 1)

        # Second user gets promo
        user2 = get_or_create_user(4011, "promo2", "Promo2")
        self.assertTrue(user2["is_promo_eligible"])
        self.assertEqual(get_promo_remaining(), 0)

        # Third user does NOT get promo
        user3 = get_or_create_user(4012, "nopromo", "NoPromo")
        self.assertFalse(user3["is_promo_eligible"])

    def test_payload_format(self):
        """Test payment payload format matches expectations."""
        # Payload format: tokens_{count}_{user_id}_{promo}
        payload = f"tokens_5_12345_True"
        parts = payload.split("_")

        self.assertEqual(parts[0], "tokens")
        self.assertEqual(int(parts[1]), 5)
        self.assertEqual(int(parts[2]), 12345)
        self.assertEqual(parts[3], "True")


class TestGiveaway(unittest.TestCase):
    """Tests for giveaway system."""

    def setUp(self):
        """Reset giveaway state before each test."""
        # Ensure giveaway is stopped before each test
        if is_giveaway_active():
            stop_giveaway()

    def test_giveaway_start_stop(self):
        """Test starting and stopping giveaway campaigns."""
        # Start giveaway
        start_giveaway(tokens=3, users=10, admin_id=12345)

        # Check active
        self.assertTrue(is_giveaway_active())
        self.assertEqual(get_giveaway_remaining(), 10)

        config = get_giveaway_config()
        self.assertEqual(config["active"], "1")
        self.assertEqual(config["tokens_per_user"], "3")
        self.assertEqual(config["remaining_users"], "10")

        # Stop giveaway
        stats = stop_giveaway()
        self.assertFalse(is_giveaway_active())
        self.assertIn("users_granted", stats)
        self.assertIn("tokens_granted", stats)

    def test_giveaway_grant_new_user(self):
        """Test that new users receive giveaway tokens."""
        # Start giveaway
        start_giveaway(tokens=5, users=100, admin_id=12345)

        # Create new user (must create in DB first)
        user_id = 5001
        get_or_create_user(user_id, "giveaway_test", "Giveaway", "Test")
        result = check_and_grant_giveaway(user_id, is_new_user=True)

        # Should be granted
        self.assertTrue(result["granted"])
        self.assertEqual(result["tokens"], 5)
        self.assertEqual(result["reason"], "success")

        # Check balance
        self.assertEqual(get_balance(user_id), 5)

        # Check flag
        user = get_user(user_id)
        self.assertEqual(user["giveaway_received"], 1)

        # Check transaction
        txns = get_user_transactions(user_id)
        self.assertEqual(len(txns), 1)
        self.assertEqual(txns[0]["type"], "giveaway")
        self.assertEqual(txns[0]["amount"], 5)

        # Check counter decreased
        self.assertEqual(get_giveaway_remaining(), 99)

    def test_giveaway_no_grant_existing(self):
        """Test that existing users don't receive giveaway tokens."""
        start_giveaway(tokens=3, users=100, admin_id=12345)

        user_id = 5002
        get_or_create_user(user_id, "existing", "Existing")
        # Existing user
        result = check_and_grant_giveaway(user_id, is_new_user=False)

        self.assertFalse(result["granted"])
        self.assertEqual(result["tokens"], 0)
        self.assertEqual(result["reason"], "existing_user")

    def test_giveaway_exhausted(self):
        """Test that no tokens are granted when slots exhausted."""
        # Start with 2 slots
        start_giveaway(tokens=3, users=2, admin_id=12345)

        # Grant to first user
        get_or_create_user(5003, "user1", "User1")
        check_and_grant_giveaway(5003, is_new_user=True)
        self.assertEqual(get_giveaway_remaining(), 1)

        # Grant to second user
        get_or_create_user(5004, "user2", "User2")
        check_and_grant_giveaway(5004, is_new_user=True)
        self.assertEqual(get_giveaway_remaining(), 0)

        # Third user should not get tokens
        get_or_create_user(5005, "user3", "User3")
        result = check_and_grant_giveaway(5005, is_new_user=True)
        self.assertFalse(result["granted"])
        self.assertEqual(result["reason"], "slots_exhausted")
        self.assertEqual(get_balance(5005), 0)

    def test_giveaway_inactive(self):
        """Test that no tokens are granted when giveaway inactive."""
        # No active giveaway
        user_id = 5006
        get_or_create_user(user_id, "inactive_test", "Inactive")
        result = check_and_grant_giveaway(user_id, is_new_user=True)

        self.assertFalse(result["granted"])
        self.assertEqual(result["reason"], "giveaway_inactive")

    def test_giveaway_double_grant(self):
        """Test that flag prevents duplicate grants."""
        start_giveaway(tokens=3, users=100, admin_id=12345)

        user_id = 5007
        get_or_create_user(user_id, "double_test", "Double")

        # First grant
        result1 = check_and_grant_giveaway(user_id, is_new_user=True)
        self.assertTrue(result1["granted"])
        self.assertEqual(get_balance(user_id), 3)

        # Try to grant again
        result2 = check_and_grant_giveaway(user_id, is_new_user=True)
        self.assertFalse(result2["granted"])
        self.assertEqual(result2["reason"], "already_received")
        self.assertEqual(get_balance(user_id), 3)  # Balance unchanged

    def test_giveaway_transaction_audit(self):
        """Test that giveaway transactions are properly logged."""
        start_giveaway(tokens=7, users=50, admin_id=12345)

        # Grant tokens
        user_id = 5008
        get_or_create_user(user_id, "audit_test", "Audit")
        check_and_grant_giveaway(user_id, is_new_user=True)

        # Check transaction details
        txns = get_user_transactions(user_id)
        self.assertEqual(len(txns), 1)

        txn = txns[0]
        self.assertEqual(txn["type"], "giveaway")
        self.assertEqual(txn["amount"], 7)
        self.assertEqual(txn["balance_before"], 0)
        self.assertEqual(txn["balance_after"], 7)
        self.assertIn("Welcome bonus", txn["description"])

    def test_giveaway_stats(self):
        """Test giveaway statistics after stopping."""
        start_giveaway(tokens=4, users=5, admin_id=12345)

        # Grant to 3 users
        get_or_create_user(5009, "stats1", "Stats1")
        result1 = check_and_grant_giveaway(5009, is_new_user=True)
        get_or_create_user(5010, "stats2", "Stats2")
        result2 = check_and_grant_giveaway(5010, is_new_user=True)
        get_or_create_user(5011, "stats3", "Stats3")
        result3 = check_and_grant_giveaway(5011, is_new_user=True)

        # Verify grants succeeded
        self.assertTrue(result1["granted"])
        self.assertTrue(result2["granted"])
        self.assertTrue(result3["granted"])

        # Stop and check stats
        stats = stop_giveaway()
        # Stats should reflect at least 3 users and 12 tokens from this test
        # (could be more due to test execution order)
        self.assertGreaterEqual(stats["users_granted"], 3)
        self.assertGreaterEqual(stats["tokens_granted"], 12)  # 3 users × 4 tokens
        self.assertEqual(stats["remaining_users"], 2)


class TestGiftPacks(unittest.TestCase):
    """Tests for gift pack system."""

    @classmethod
    def setUpClass(cls):
        """Initialize test database."""
        init_db(Path(TEST_DB_PATH))

    def test_campaign_create_and_list(self):
        """Test creating and listing campaigns."""
        camp_id = create_gift_pack_campaign(
            campaign_type="new_user",
            tokens=3,
            expiration_days=7,
            remaining_slots=100,
            description="Test campaign"
        )
        self.assertGreater(camp_id, 0)

        campaigns = get_active_campaigns()
        self.assertTrue(any(c["id"] == camp_id for c in campaigns))

    def test_campaign_deactivate(self):
        """Test deactivating a campaign."""
        camp_id = create_gift_pack_campaign(
            campaign_type="new_user", tokens=1, expiration_days=1, remaining_slots=10
        )
        self.assertTrue(deactivate_campaign(camp_id))

        # Should not appear in active campaigns
        active = get_active_campaigns()
        self.assertFalse(any(c["id"] == camp_id for c in active))

        # But should appear in all campaigns
        all_camps = get_all_campaigns(include_inactive=True)
        self.assertTrue(any(c["id"] == camp_id for c in all_camps))

    def test_deeplink_campaign_matching(self):
        """Test deeplink campaign matching logic."""
        camp_id = create_gift_pack_campaign(
            campaign_type="deeplink",
            tokens=5,
            expiration_days=3,
            ref_source="test_ads",
            remaining_slots=50
        )

        # Should match with correct ref_source
        match = get_matching_campaign(is_new_user=True, ref_source="test_ads")
        self.assertIsNotNone(match)
        self.assertEqual(match["id"], camp_id)

        # Should not match with wrong ref_source
        match2 = get_matching_campaign(is_new_user=True, ref_source="other_source")
        # May match a new_user campaign if one exists, but not this deeplink one
        if match2:
            self.assertNotEqual(match2["campaign_type"], "deeplink")

    def test_grant_and_redeem_flow(self):
        """Test full flow: campaign → grant → redeem → no double grant/redeem."""
        # Create campaign
        create_gift_pack_campaign(
            campaign_type="new_user", tokens=3, expiration_days=7, remaining_slots=100
        )

        user_id = 7001
        get_or_create_user(user_id, "gp_test1", "GP", "Test1")

        # Grant gift pack
        result = check_and_grant_gift_pack(user_id, is_new_user=True)
        self.assertTrue(result["granted"])
        self.assertEqual(result["tokens"], 3)
        self.assertEqual(result["expiration_days"], 7)

        # Balance should still be 0 (pack not redeemed yet)
        self.assertEqual(get_balance(user_id), 0)

        # User should have an active pack
        packs = get_user_active_packs(user_id)
        self.assertEqual(len(packs), 1)
        self.assertEqual(packs[0]["tokens"], 3)
        self.assertEqual(packs[0]["status"], "active")

        # Flag should be set
        self.assertTrue(has_received_gift_pack(user_id))

        # Double grant should fail
        result2 = check_and_grant_gift_pack(user_id, is_new_user=True)
        self.assertFalse(result2["granted"])
        self.assertEqual(result2["reason"], "already_received")

        # Redeem the pack
        balance_before = get_balance(user_id)
        redeem_result = check_and_redeem_gift_pack(user_id)
        self.assertTrue(redeem_result["redeemed"])
        self.assertEqual(redeem_result["tokens"], 3)

        # Balance should increase
        self.assertEqual(get_balance(user_id), balance_before + 3)

        # Pack should be redeemed
        packs = get_user_active_packs(user_id)
        self.assertEqual(len(packs), 0)

        # Transaction should be logged
        txns = get_user_transactions(user_id)
        gift_txns = [t for t in txns if t["type"] == "gift_pack_redeem"]
        self.assertGreater(len(gift_txns), 0)
        self.assertEqual(gift_txns[0]["amount"], 3)

        # Double redeem should fail (no more active packs)
        redeem_result2 = check_and_redeem_gift_pack(user_id)
        self.assertFalse(redeem_result2["redeemed"])

    def test_gift_pack_expire(self):
        """Test expiring overdue packs."""
        user_id = 7002
        get_or_create_user(user_id, "gp_expire", "Expire")

        # Create pack manually
        pack_id = create_gift_pack(user_id, tokens=5, expiration_days=7)
        self.assertGreater(pack_id, 0)

        # Manually set expires_at to past
        with get_connection() as conn:
            conn.cursor().execute(
                "UPDATE gift_packs SET expires_at = datetime('now', '-1 hour') WHERE id = ?",
                (pack_id,)
            )

        # Expire packs
        expired = expire_gift_packs()
        self.assertGreaterEqual(expired, 1)

        # Should not be redeemable
        result = check_and_redeem_gift_pack(user_id)
        self.assertFalse(result["redeemed"])

    def test_gift_pack_stats(self):
        """Test gift pack statistics."""
        stats = get_gift_pack_stats()
        self.assertIn("by_status", stats)
        self.assertIn("by_campaign", stats)
        self.assertIn("total", stats)
        self.assertGreater(stats["total"], 0)

    def test_campaign_slot_decrement(self):
        """Test campaign slot decrement."""
        camp_id = create_gift_pack_campaign(
            campaign_type="new_user", tokens=1, expiration_days=1, remaining_slots=5
        )
        decrement_campaign_slots(camp_id)

        campaigns = get_all_campaigns()
        camp = next(c for c in campaigns if c["id"] == camp_id)
        self.assertEqual(camp["remaining_slots"], 4)

    def test_inactive_users_query(self):
        """Test getting inactive users."""
        # Create a user who never purchased or used tokens
        user_id = 7010
        get_or_create_user(user_id, "inactive_gp", "Inactive")

        inactive = get_inactive_users()
        inactive_ids = [u["user_id"] for u in inactive]
        self.assertIn(user_id, inactive_ids)

    def test_deeplink_priority_over_new_user(self):
        """Test deeplink campaign takes priority over new_user."""
        # Create both types
        new_user_camp = create_gift_pack_campaign(
            campaign_type="new_user", tokens=2, expiration_days=7, remaining_slots=100
        )
        deeplink_camp = create_gift_pack_campaign(
            campaign_type="deeplink", tokens=10, expiration_days=14,
            ref_source="priority_test", remaining_slots=50
        )

        # Matching deeplink should win
        match = get_matching_campaign(is_new_user=True, ref_source="priority_test")
        self.assertIsNotNone(match)
        self.assertEqual(match["campaign_type"], "deeplink")
        self.assertEqual(match["tokens"], 10)

    def test_unlimited_slots_campaign(self):
        """Test campaign with unlimited slots (-1) grants without decrementing."""
        # Deactivate existing campaigns first
        for c in get_all_campaigns():
            deactivate_campaign(c["id"])

        camp_id = create_gift_pack_campaign(
            campaign_type="new_user", tokens=2, expiration_days=3, remaining_slots=-1
        )

        # Grant to user
        user_id = 7030
        get_or_create_user(user_id, "unlimited_test", "Unlimited")
        result = check_and_grant_gift_pack(user_id, is_new_user=True)
        self.assertTrue(result["granted"])
        self.assertEqual(result["tokens"], 2)

        # remaining_slots should still be -1 (not decremented)
        camps = get_all_campaigns()
        camp = next(c for c in camps if c["id"] == camp_id)
        self.assertEqual(camp["remaining_slots"], -1)

    def test_slots_exhausted_at_zero(self):
        """Test that remaining_slots=0 means exhausted, not unlimited."""
        # Deactivate existing campaigns first
        for c in get_all_campaigns():
            deactivate_campaign(c["id"])

        # Create campaign with 1 slot
        camp_id = create_gift_pack_campaign(
            campaign_type="new_user", tokens=2, expiration_days=3, remaining_slots=1
        )

        # First user takes the last slot
        user_id = 7031
        get_or_create_user(user_id, "last_slot", "LastSlot")
        result = check_and_grant_gift_pack(user_id, is_new_user=True)
        self.assertTrue(result["granted"])

        # Campaign should now have 0 remaining
        camps = get_all_campaigns(include_inactive=True)
        camp = next(c for c in camps if c["id"] == camp_id)
        self.assertEqual(camp["remaining_slots"], 0)

        # Second user should NOT get a pack (0 is exhausted, not unlimited)
        user_id2 = 7032
        get_or_create_user(user_id2, "too_late", "TooLate")
        result2 = check_and_grant_gift_pack(user_id2, is_new_user=True)
        self.assertFalse(result2["granted"])

    def test_no_campaign_no_grant(self):
        """Test that without campaigns, no packs are granted."""
        # Deactivate all campaigns
        all_camps = get_all_campaigns()
        for c in all_camps:
            deactivate_campaign(c["id"])

        user_id = 7020
        get_or_create_user(user_id, "no_camp", "NoCamp")
        result = check_and_grant_gift_pack(user_id, is_new_user=True)
        self.assertFalse(result["granted"])
        self.assertEqual(result["reason"], "no_campaigns")


class TestSurvey(unittest.TestCase):
    """Tests for survey system."""

    @classmethod
    def setUpClass(cls):
        """Initialize test database."""
        init_db(Path(TEST_DB_PATH))

    def test_create_survey_with_defaults(self):
        """Test creating a survey with default questions."""
        sid = create_survey("Test Survey", created_by=1)
        self.assertGreater(sid, 0)

        survey = get_survey(sid)
        self.assertEqual(survey["title"], "Test Survey")
        self.assertEqual(survey["reward_tokens"], 3)
        self.assertEqual(survey["reward_days"], 7)
        self.assertEqual(survey["is_active"], 1)

        import json
        questions = json.loads(survey["questions_json"])
        self.assertEqual(len(questions), 7)
        self.assertEqual(questions[0]["key"], "video_quality")
        self.assertEqual(questions[6]["key"], "improvement")

    def test_active_surveys(self):
        """Test listing active surveys."""
        sid = create_survey("Active Survey")
        surveys = get_active_surveys()
        self.assertTrue(any(s["id"] == sid for s in surveys))

    def test_deactivate_survey(self):
        """Test deactivating a survey."""
        sid = create_survey("Deactivate Me")
        self.assertTrue(deactivate_survey(sid))

        survey = get_survey(sid)
        self.assertEqual(survey["is_active"], 0)

        # Should not appear in active list
        active = get_active_surveys()
        self.assertFalse(any(s["id"] == sid for s in active))

    def test_invitation_create_and_unique(self):
        """Test invitation creation and uniqueness constraint."""
        sid = create_survey("Invite Test")
        user_id = 8001
        get_or_create_user(user_id, "survey_user1", "Survey1")

        inv_id = create_invitation(sid, user_id)
        self.assertIsNotNone(inv_id)

        inv = get_invitation(sid, user_id)
        self.assertEqual(inv["status"], "sent")
        self.assertEqual(inv["survey_id"], sid)
        self.assertEqual(inv["user_id"], user_id)

        # Duplicate should return None
        dup = create_invitation(sid, user_id)
        self.assertIsNone(dup)

    def test_invitation_status_flow(self):
        """Test invitation status transitions: sent → started → completed."""
        sid = create_survey("Status Flow Test")
        user_id = 8002
        get_or_create_user(user_id, "survey_user2", "Survey2")

        inv_id = create_invitation(sid, user_id)

        # sent → started
        update_invitation_status(inv_id, "started")
        inv = get_invitation_by_id(inv_id)
        self.assertEqual(inv["status"], "started")
        self.assertIsNotNone(inv["started_at"])

        # started → completed
        update_invitation_status(inv_id, "completed")
        inv = get_invitation_by_id(inv_id)
        self.assertEqual(inv["status"], "completed")
        self.assertIsNotNone(inv["completed_at"])

    def test_save_and_get_responses(self):
        """Test saving and retrieving survey responses."""
        sid = create_survey("Response Test")
        user_id = 8003
        get_or_create_user(user_id, "survey_user3", "Survey3")
        inv_id = create_invitation(sid, user_id)

        save_response(inv_id, sid, user_id, "video_quality", "4")
        save_response(inv_id, sid, user_id, "nps", "8")
        save_response(inv_id, sid, user_id, "stop_reason", "0|2|5")

        responses = get_user_responses(inv_id)
        self.assertEqual(len(responses), 3)
        self.assertEqual(responses["video_quality"], "4")
        self.assertEqual(responses["nps"], "8")
        self.assertEqual(responses["stop_reason"], "0|2|5")

    def test_response_upsert(self):
        """Test that save_response replaces existing answers (UNIQUE constraint)."""
        sid = create_survey("Upsert Test")
        user_id = 8004
        get_or_create_user(user_id, "survey_user4", "Survey4")
        inv_id = create_invitation(sid, user_id)

        save_response(inv_id, sid, user_id, "nps", "5")
        save_response(inv_id, sid, user_id, "nps", "9")

        responses = get_user_responses(inv_id)
        self.assertEqual(responses["nps"], "9")

    def test_reward_flag(self):
        """Test marking invitation as rewarded."""
        sid = create_survey("Reward Test")
        user_id = 8005
        get_or_create_user(user_id, "survey_user5", "Survey5")
        inv_id = create_invitation(sid, user_id)

        inv = get_invitation_by_id(inv_id)
        self.assertEqual(inv["rewarded"], 0)

        mark_invitation_rewarded(inv_id)
        inv = get_invitation_by_id(inv_id)
        self.assertEqual(inv["rewarded"], 1)

    def test_full_survey_flow(self):
        """Test complete flow: create → invite → answer all → complete → stats."""
        sid = create_survey("Full Flow", reward_tokens=3, reward_days=7)
        user_id = 8006
        get_or_create_user(user_id, "survey_full", "FullFlow")
        inv_id = create_invitation(sid, user_id)

        update_invitation_status(inv_id, "started")

        # Answer all 7 questions
        save_response(inv_id, sid, user_id, "video_quality", "4")
        save_response(inv_id, sid, user_id, "video_usage", "0")
        save_response(inv_id, sid, user_id, "stop_reason", "0|3")
        save_response(inv_id, sid, user_id, "fair_price", "2")
        save_response(inv_id, sid, user_id, "voice_messages", "3")
        save_response(inv_id, sid, user_id, "nps", "8")
        save_response(inv_id, sid, user_id, "improvement", "Better quality")

        responses = get_user_responses(inv_id)
        self.assertEqual(len(responses), 7)

        update_invitation_status(inv_id, "completed")
        mark_invitation_rewarded(inv_id)

        # Verify stats
        stats = get_survey_stats(sid)
        self.assertEqual(stats["completed"], 1)
        self.assertEqual(stats["completion_rate"], 100.0)
        self.assertIn("video_quality", stats["questions"])
        self.assertIn("nps", stats["questions"])
        # NPS: 8 is passive (not promoter, not detractor)
        self.assertIsNotNone(stats["nps_score"])

    def test_survey_stats_nps_calculation(self):
        """Test NPS score calculation with multiple users."""
        sid = create_survey("NPS Test")

        # Promoter (score 9)
        uid1 = 8010
        get_or_create_user(uid1, "nps1", "NPS1")
        inv1 = create_invitation(sid, uid1)
        save_response(inv1, sid, uid1, "nps", "9")
        update_invitation_status(inv1, "completed")

        # Detractor (score 5)
        uid2 = 8011
        get_or_create_user(uid2, "nps2", "NPS2")
        inv2 = create_invitation(sid, uid2)
        save_response(inv2, sid, uid2, "nps", "5")
        update_invitation_status(inv2, "completed")

        # Passive (score 7)
        uid3 = 8012
        get_or_create_user(uid3, "nps3", "NPS3")
        inv3 = create_invitation(sid, uid3)
        save_response(inv3, sid, uid3, "nps", "7")
        update_invitation_status(inv3, "completed")

        stats = get_survey_stats(sid)
        # NPS = (promoters - detractors) / total * 100
        # 1 promoter, 1 detractor, 1 passive → (1 - 1) / 3 * 100 = 0
        self.assertEqual(stats["nps_score"], 0)

    def test_target_users_query(self):
        """Test getting target users for survey (inactive giveaway/gift recipients)."""
        # Create a user who got a giveaway but has no balance and never purchased
        uid = 8020
        get_or_create_user(uid, "inactive_target", "Inactive")
        update_user(uid, {"giveaway_received": 1, "balance": 0, "total_purchased": 0})

        targets = get_survey_target_users(limit=100)
        target_ids = [u["user_id"] for u in targets]
        self.assertIn(uid, target_ids)

    def test_export_responses(self):
        """Test exporting all responses for a survey."""
        sid = create_survey("Export Test")
        uid = 8030
        get_or_create_user(uid, "export_user", "Export")
        inv_id = create_invitation(sid, uid)
        save_response(inv_id, sid, uid, "video_quality", "5")
        save_response(inv_id, sid, uid, "nps", "10")
        update_invitation_status(inv_id, "completed")

        export = get_all_responses_export(sid)
        self.assertEqual(len(export), 1)
        self.assertEqual(export[0]["user_id"], uid)
        self.assertIn("video_quality", export[0]["answers"])
        self.assertEqual(export[0]["answers"]["video_quality"], "5")

    def test_default_questions_structure(self):
        """Test DEFAULT_QUESTIONS constant has correct structure."""
        self.assertEqual(len(DEFAULT_QUESTIONS), 7)

        keys = [q["key"] for q in DEFAULT_QUESTIONS]
        self.assertEqual(keys, [
            "video_quality", "video_usage", "stop_reason",
            "fair_price", "voice_messages", "nps", "improvement",
        ])

        types = [q["type"] for q in DEFAULT_QUESTIONS]
        self.assertEqual(types, [
            "rating", "single", "multi", "single", "single", "nps", "text",
        ])

        # Check optional flag on improvement
        self.assertTrue(DEFAULT_QUESTIONS[6].get("optional"))


def cleanup():
    """Remove test database."""
    try:
        os.unlink(TEST_DB_PATH)
        print(f"\nCleaned up test database: {TEST_DB_PATH}")
    except Exception as e:
        print(f"Cleanup warning: {e}")


if __name__ == "__main__":
    print(f"Test database: {TEST_DB_PATH}\n")

    # Run tests
    loader = unittest.TestLoader()
    suite = unittest.TestSuite()

    # Add test classes
    suite.addTests(loader.loadTestsFromTestCase(TestDatabase))
    suite.addTests(loader.loadTestsFromTestCase(TestTokenManager))
    suite.addTests(loader.loadTestsFromTestCase(TestIntegration))
    suite.addTests(loader.loadTestsFromTestCase(TestPaymentHandlers))
    suite.addTests(loader.loadTestsFromTestCase(TestGiveaway))
    suite.addTests(loader.loadTestsFromTestCase(TestGiftPacks))
    suite.addTests(loader.loadTestsFromTestCase(TestSurvey))

    # Run with verbosity
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(suite)

    # Cleanup
    cleanup()

    # Exit with appropriate code
    sys.exit(0 if result.wasSuccessful() else 1)
