import pytest
import json
import random
import string
from hypothesis import given, settings, Phase
from hypothesis import strategies as st
from app import app, mysql, get_role_permissions, PERMISSION_KEYS, NOTIF_KEYS, ALL_TRUE_PERMISSIONS, ALL_FALSE_PERMISSIONS

# Hardcoded original permissions to test against (Property 5)
ORIGINAL_PERMISSIONS = {
    'SUPER ADMIN': {k: True for k in PERMISSION_KEYS},
    'DIRECTION': {
        **{k: True for k in ['interventions','equipements','pieces','preventifs','ordres',
                              'fournisseurs','commandes','familles','operations','operateurs',
                              'qualifications','outillages','gammes','constats','rapports',
                              'utilisateurs','can_edit','can_add','activites','export_data','valider_demande']},
        'can_delete': False, 'roles': False
    },
    'CHEF DEPARTEMENT': {
        **{k: True for k in ['interventions','equipements','pieces','preventifs','ordres',
                              'fournisseurs','commandes','familles','operations','operateurs',
                              'qualifications','outillages','gammes','constats','rapports',
                              'can_edit','can_add','export_data','valider_demande']},
        'utilisateurs': False, 'can_delete': False, 'roles': False, 'activites': False
    },
    'STM': {
        'interventions': True, 'equipements': True, 'pieces': True, 'preventifs': True, 'ordres': True,
        'fournisseurs': True, 'commandes': True, 'familles': True, 'operations': True, 'operateurs': True,
        'qualifications': True, 'outillages': True, 'gammes': True, 'constats': True, 'rapports': True,
        'utilisateurs': False, 'roles': False, 'activites': False,
        'can_edit': True, 'can_add': True, 'can_delete': True,
        'export_data': False, 'valider_demande': True
    },
    'SIM': {
        'interventions': True, 'equipements': True, 'pieces': True, 'preventifs': True, 'ordres': True,
        'fournisseurs': True, 'commandes': True, 'familles': True, 'operations': True, 'operateurs': True,
        'qualifications': True, 'outillages': True, 'gammes': True, 'constats': True, 'rapports': True,
        'utilisateurs': False, 'roles': False, 'activites': False,
        'can_edit': True, 'can_add': True, 'can_delete': False,
        'export_data': False, 'valider_demande': True
    },
    'CEM': {
        'interventions': True, 'equipements': True, 'pieces': True, 'preventifs': True, 'ordres': True,
        'fournisseurs': True, 'commandes': True, 'familles': True, 'operations': True, 'operateurs': True,
        'qualifications': True, 'outillages': True, 'gammes': True, 'constats': True, 'rapports': True,
        'utilisateurs': False, 'roles': False, 'activites': False,
        'can_edit': True, 'can_add': True, 'can_delete': True,
        'export_data': False, 'valider_demande': True
    },
    'PRODUCTION': {
        'interventions': True, 'equipements': False, 'pieces': False, 'preventifs': False, 'ordres': False,
        'fournisseurs': False, 'commandes': False, 'familles': False, 'operations': False, 'operateurs': False,
        'qualifications': False, 'outillages': False, 'gammes': False, 'constats': False, 'rapports': False,
        'utilisateurs': False, 'roles': False, 'activites': False,
        'can_edit': False, 'can_add': True, 'can_delete': False,
        'export_data': False, 'valider_demande': False
    },
    'LABORATOIRE': {
        'interventions': True, 'equipements': False, 'pieces': False, 'preventifs': False, 'ordres': False,
        'fournisseurs': False, 'commandes': False, 'familles': False, 'operations': False, 'operateurs': False,
        'qualifications': False, 'outillages': False, 'gammes': False, 'constats': False, 'rapports': False,
        'utilisateurs': False, 'roles': False, 'activites': False,
        'can_edit': False, 'can_add': True, 'can_delete': False,
        'export_data': False, 'valider_demande': False
    },
    'AQ': {
        'interventions': True, 'equipements': True, 'pieces': False, 'preventifs': True, 'ordres': False,
        'fournisseurs': False, 'commandes': False, 'familles': False, 'operations': False, 'operateurs': False,
        'qualifications': False, 'outillages': False, 'gammes': False, 'constats': True, 'rapports': True,
        'utilisateurs': False, 'roles': False, 'activites': False,
        'can_edit': True, 'can_add': True, 'can_delete': False,
        'export_data': False, 'valider_demande': False
    },
    'UTILITE': {
        'interventions': True, 'equipements': False, 'pieces': False, 'preventifs': False, 'ordres': False,
        'fournisseurs': False, 'commandes': False, 'familles': False, 'operations': False, 'operateurs': False,
        'qualifications': False, 'outillages': False, 'gammes': False, 'constats': False, 'rapports': False,
        'utilisateurs': False, 'roles': False, 'activites': False,
        'can_edit': False, 'can_add': True, 'can_delete': False,
        'export_data': False, 'valider_demande': False
    }
}

# Helper to generate random string
def rand_str(length=10):
    return ''.join(random.choices(string.ascii_letters, k=length))

@pytest.fixture(scope="module", autouse=True)
def app_context():
    with app.app_context():
        yield

@pytest.fixture(autouse=True)
def clean_db():
    # Clean up test roles and test users before and after each test
    cursor = mysql.connection.cursor()
    cursor.execute("DELETE FROM user WHERE username LIKE 'TEST_USR_%'")
    cursor.execute("DELETE FROM roles WHERE name LIKE 'TEST_ROLE_%'")
    mysql.connection.commit()
    cursor.close()
    
    yield
    
    cursor = mysql.connection.cursor()
    cursor.execute("DELETE FROM user WHERE username LIKE 'TEST_USR_%'")
    cursor.execute("DELETE FROM roles WHERE name LIKE 'TEST_ROLE_%'")
    mysql.connection.commit()
    cursor.close()

# Feature: dynamic-role-management, Property 1: SUPER ADMIN invariant
def test_property_1_super_admin_invariant():
    # SUPER ADMIN should always return all True even if a row exists with False permissions in DB
    cursor = mysql.connection.cursor()
    # Insert a malicious row to try to overwrite SUPER ADMIN permissions to False
    cursor.execute("SELECT id FROM roles WHERE name = 'SUPER ADMIN' LIMIT 1")
    row = cursor.fetchone()
    if row:
        # Update everything to 0
        set_clause = ", ".join([f"`{k}` = 0" for k in PERMISSION_KEYS])
        cursor.execute(f"UPDATE roles SET {set_clause} WHERE name = 'SUPER ADMIN'")
        mysql.connection.commit()
    cursor.close()

    res = get_role_permissions('SUPER ADMIN')
    assert all(res[k] is True for k in PERMISSION_KEYS)
    
    # Restore it back via a fresh migration run or manual update
    from migrate_roles import migrate
    migrate(dry_run=False)

# Feature: dynamic-role-management, Property 2: Unknown role falls back to all-False
@given(st.text(min_size=1, max_size=20).filter(lambda x: x not in ORIGINAL_PERMISSIONS))
@settings(max_examples=50, deadline=None)
def test_property_2_unknown_role_fallback(role_name):
    res = get_role_permissions(role_name)
    assert all(res[k] is False for k in PERMISSION_KEYS)

# Feature: dynamic-role-management, Property 3: Permission resolver schema invariance
@given(st.sampled_from(list(ORIGINAL_PERMISSIONS.keys())))
@settings(max_examples=10)
def test_property_3_schema_invariance(role_name):
    res = get_role_permissions(role_name)
    assert set(res.keys()) == set(PERMISSION_KEYS)

# Feature: dynamic-role-management, Property 4: Permission round-trip
@given(
    role_suffix=st.text(alphabet=string.ascii_lowercase, min_size=5, max_size=10),
    perms=st.fixed_dictionaries({k: st.booleans() for k in PERMISSION_KEYS})
)
@settings(max_examples=20, deadline=None)
def test_property_4_permission_round_trip(role_suffix, perms):
    role_name = f"TEST_ROLE_{role_suffix}"
    cursor = mysql.connection.cursor()
    
    # Delete first in case of Hypothesis duplicates
    cursor.execute("DELETE FROM roles WHERE name = %s", (role_name,))
    
    # Save the role
    columns = ['name', 'description', 'is_builtin', 'color', 'icon', 'notif_config_default'] + list(perms.keys())
    values = [role_name, 'Test Description', 0, '#6366f1', 'fa-user-tag', '{}'] + [1 if perms[k] else 0 for k in perms]
    
    placeholders = ", ".join(["%s"] * len(columns))
    cols_str = ", ".join([f"`{c}`" for c in columns])
    sql = f"INSERT INTO roles ({cols_str}) VALUES ({placeholders})"
    cursor.execute(sql, tuple(values))
    mysql.connection.commit()
    cursor.close()
    
    # Resolve and verify
    resolved = get_role_permissions(role_name)
    for k in PERMISSION_KEYS:
        assert resolved[k] == perms[k]

# Feature: dynamic-role-management, Property 5: Built-in migration equivalence
@pytest.mark.parametrize("role_name", list(ORIGINAL_PERMISSIONS.keys()))
def test_property_5_migration_equivalence(role_name):
    res = get_role_permissions(role_name)
    for k in PERMISSION_KEYS:
        assert res[k] == ORIGINAL_PERMISSIONS[role_name][k]

# Feature: dynamic-role-management, Property 6: Migration idempotence
def test_property_6_migration_idempotence():
    from migrate_roles import migrate
    # Running migration twice should result in the same database state
    migrate(dry_run=False)
    
    cursor = mysql.connection.cursor()
    cursor.execute("SELECT * FROM roles ORDER BY name")
    state_1 = cursor.fetchall()
    
    migrate(dry_run=False)
    cursor.execute("SELECT * FROM roles ORDER BY name")
    state_2 = cursor.fetchall()
    cursor.close()
    
    assert len(state_1) == len(state_2)
    for r1, r2 in zip(state_1, state_2):
        assert r1['name'] == r2['name']
        for k in PERMISSION_KEYS:
            assert r1[k] == r2[k]

# Feature: dynamic-role-management, Property 7: notif_config preserved through migration
def test_property_7_notif_config_preserved():
    cursor = mysql.connection.cursor()
    test_username = 'TEST_USR_PRESERVE'
    cursor.execute("DELETE FROM user WHERE username = %s", (test_username,))
    
    # Create user with a custom notif_config
    custom_notif = json.dumps({'nouvelle_intervention': '1', 'ordre_travail': '1'})
    cursor.execute("""
        INSERT INTO user (username, password, type, connect, notif_config)
        VALUES (%s, 'dummy', 'PRODUCTION', 'oui', %s)
    """, (test_username, custom_notif))
    mysql.connection.commit()
    
    # Run migration
    from migrate_roles import migrate
    migrate(dry_run=False)
    
    # Verify user's notif_config is unchanged
    cursor.execute("SELECT notif_config FROM user WHERE username = %s", (test_username,))
    user = cursor.fetchone()
    cursor.execute("DELETE FROM user WHERE username = %s", (test_username,))
    mysql.connection.commit()
    cursor.close()
    
    assert user['notif_config'] == custom_notif

# Feature: dynamic-role-management, Property 8: New user notif_config initialized from role defaults
def test_property_8_new_user_notif_init():
    role_name = 'TEST_ROLE_NOTIF'
    default_notif = {'nouvelle_intervention': '0', 'intervention_urgente': '1', 'ordre_travail': '1'}
    
    cursor = mysql.connection.cursor()
    # Insert custom role with notifications
    columns = ['name', 'is_builtin', 'notif_config_default']
    values = [role_name, 0, json.dumps(default_notif)]
    sql = f"INSERT INTO roles ({', '.join(columns)}) VALUES (%s, %s, %s)"
    cursor.execute(sql, tuple(values))
    mysql.connection.commit()
    
    # Simulate user creation route behavior
    test_username = 'TEST_USR_NEW'
    cursor.execute("SELECT notif_config_default FROM roles WHERE name = %s LIMIT 1", (role_name,))
    role_data = cursor.fetchone()
    notif_config_str = role_data['notif_config_default']
    
    cursor.execute("""
        INSERT INTO user (username, password, type, connect, notif_config)
        VALUES (%s, 'dummy', %s, 'oui', %s)
    """, (test_username, role_name, notif_config_str))
    mysql.connection.commit()
    
    # Verify initialized config
    cursor.execute("SELECT notif_config FROM user WHERE username = %s", (test_username,))
    user = cursor.fetchone()
    
    # Cleanup
    cursor.execute("DELETE FROM user WHERE username = %s", (test_username,))
    cursor.execute("DELETE FROM roles WHERE name = %s", (role_name,))
    mysql.connection.commit()
    cursor.close()
    
    assert json.loads(user['notif_config']) == default_notif

# Feature: dynamic-role-management, Property 9: Role change does not overwrite user notif_config
def test_property_9_role_change_preserves_notif():
    test_username = 'TEST_USR_CHANGE'
    custom_notif = json.dumps({'nouvelle_intervention': '1'})
    
    cursor = mysql.connection.cursor()
    cursor.execute("""
        INSERT INTO user (username, password, type, connect, notif_config)
        VALUES (%s, 'dummy', 'PRODUCTION', 'oui', %s)
    """, (test_username, custom_notif))
    mysql.connection.commit()
    
    # Change user's role to AQ (mimics update profile)
    cursor.execute("UPDATE user SET type = 'AQ' WHERE username = %s", (test_username,))
    mysql.connection.commit()
    
    # Verify notif_config remains unchanged
    cursor.execute("SELECT notif_config FROM user WHERE username = %s", (test_username,))
    user = cursor.fetchone()
    
    # Cleanup
    cursor.execute("DELETE FROM user WHERE username = %s", (test_username,))
    mysql.connection.commit()
    cursor.close()
    
    assert user['notif_config'] == custom_notif

# Feature: dynamic-role-management, Property 10: Reset to role defaults round-trip
def test_property_10_reset_notif_to_defaults():
    role_name = 'TEST_ROLE_RESET'
    default_notif = {'nouvelle_intervention': '0', 'intervention_traitee': '1'}
    test_username = 'TEST_USR_RESET'
    
    cursor = mysql.connection.cursor()
    # Insert custom role
    cursor.execute("INSERT INTO roles (name, notif_config_default) VALUES (%s, %s)", (role_name, json.dumps(default_notif)))
    # Insert user with completely different notif_config
    cursor.execute("""
        INSERT INTO user (username, password, type, connect, notif_config)
        VALUES (%s, 'dummy', %s, 'oui', '{\"nouvelle_intervention\":\"1\"}')
    """, (test_username, role_name))
    mysql.connection.commit()
    
    # Reset config action (mimics route)
    cursor.execute("SELECT notif_config_default FROM roles WHERE name = %s LIMIT 1", (role_name,))
    role = cursor.fetchone()
    cursor.execute("UPDATE user SET notif_config = %s WHERE username = %s", (role['notif_config_default'], test_username))
    mysql.connection.commit()
    
    # Verify reset
    cursor.execute("SELECT notif_config FROM user WHERE username = %s", (test_username,))
    user = cursor.fetchone()
    
    # Cleanup
    cursor.execute("DELETE FROM user WHERE username = %s", (test_username,))
    cursor.execute("DELETE FROM roles WHERE name = %s", (role_name,))
    mysql.connection.commit()
    cursor.close()
    
    assert json.loads(user['notif_config']) == default_notif

# Feature: dynamic-role-management, Property 11: Role deletion blocked when users assigned
def test_property_11_delete_blocked_when_users():
    role_name = 'TEST_ROLE_DEL_BLOCKED'
    test_username = 'TEST_USR_DEL'
    
    cursor = mysql.connection.cursor()
    cursor.execute("INSERT INTO roles (name) VALUES (%s)", (role_name,))
    cursor.execute("INSERT INTO user (username, password, type) VALUES (%s, 'dummy', %s)", (test_username, role_name))
    mysql.connection.commit()
    
    # Attempt delete (mimics route guard)
    cursor.execute("SELECT COUNT(*) as user_count FROM user WHERE type = %s", (role_name,))
    user_count = cursor.fetchone()['user_count']
    
    deleted = False
    if user_count == 0:
        cursor.execute("DELETE FROM roles WHERE name = %s", (role_name,))
        mysql.connection.commit()
        deleted = True
        
    # Verify role still exists
    cursor.execute("SELECT id FROM roles WHERE name = %s", (role_name,))
    role_exists = cursor.fetchone() is not None
    
    # Cleanup
    cursor.execute("DELETE FROM user WHERE username = %s", (test_username,))
    cursor.execute("DELETE FROM roles WHERE name = %s", (role_name,))
    mysql.connection.commit()
    cursor.close()
    
    assert not deleted
    assert role_exists

# Feature: dynamic-role-management, Property 12: Built-in role deletion always blocked
@pytest.mark.parametrize("role_name", list(ORIGINAL_PERMISSIONS.keys()))
def test_property_12_builtin_delete_always_blocked(role_name):
    cursor = mysql.connection.cursor()
    cursor.execute("SELECT is_builtin FROM roles WHERE name = %s LIMIT 1", (role_name,))
    role = cursor.fetchone()
    
    deleted = False
    if role and not role['is_builtin']:
        cursor.execute("DELETE FROM roles WHERE name = %s", (role_name,))
        mysql.connection.commit()
        deleted = True
        
    assert not deleted
    cursor.close()
