import os
import django
from django.test import TestCase, override_settings
from django.core.management import call_command
from django.db import connection
from django.apps import apps
from django.contrib.auth.models import User
from decimal import Decimal
import json

# Set up Django environment
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
django.setup()

from apps.students.models import Student, ACSSEESubject
from apps.institutions.models import Institution, Programme, AdmissionLink
from apps.recommendations.models import Recommendation
from apps.courses.services import CourseRecommendationEngine


class MigrationTest(TestCase):
    """Test migrations and database setup"""
    
    def test_migrations_applied(self):
        """Test that all migrations are applied"""
        from django.db.migrations.executor import MigrationExecutor
        executor = MigrationExecutor(connection)
        
        # Get all applied migrations
        applied_migrations = executor.loader.applied_migrations
        
        # Check that our apps have migrations
        apps_to_check = ['students', 'institutions', 'recommendations']
        
        for app_name in apps_to_check:
            app_migrations = [
                migration for migration in applied_migrations 
                if migration[0] == app_name
            ]
            self.assertTrue(len(app_migrations) > 0, f"No migrations found for {app_name}")
    
    def test_database_tables_exist(self):
        """Test that database tables exist"""
        from django.db import connection
        
        tables_to_check = [
            'students_student',
            'students_acseesubject',
            'institutions_institution',
            'institutions_programme',
            'institutions_admissionlink',
            'recommendations_recommendation'
        ]
        
        with connection.cursor() as cursor:
            cursor.execute("""
                SELECT table_name 
                FROM information_schema.tables 
                WHERE table_schema = 'public'
            """)
            existing_tables = [row[0] for row in cursor.fetchall()]
        
        for table in tables_to_check:
            self.assertIn(table, existing_tables, f"Table {table} does not exist")
    
    def test_indexes_created(self):
        """Test that database indexes are created"""
        with connection.cursor() as cursor:
            cursor.execute("""
                SELECT indexname 
                FROM pg_indexes 
                WHERE schemaname = 'public'
            """)
            existing_indexes = [row[0] for row in cursor.fetchall()]
        
        # Check for specific indexes
        indexes_to_check = [
            'idx_students_search',
            'idx_subjects_search',
            'idx_programmes_search',
            'idx_recommendations_search'
        ]
        
        # Some indexes might not exist in test environment
        # Skip if not found, but log warning
        for index in indexes_to_check:
            if index not in existing_indexes:
                print(f"Warning: Index {index} not found")


class CourseRecommendationTest(TestCase):
    """Test course recommendation functionality"""
    
    @classmethod
    def setUpTestData(cls):
        """Set up test data once for all test methods"""
        print("Setting up test data...")
        
        # Create test student
        cls.student = Student.objects.create(
            candidate_number='S0306-0005',
            full_name='John Doe',
            examination_year=2023,
            sex='M'
        )
        
        # Add subjects with high grades
        subjects = [
            ('PHY', 'Physics', 'A', 5),
            ('CHE', 'Chemistry', 'A', 5),
            ('BIO', 'Biology', 'B', 4),
            ('MAT', 'Mathematics', 'A', 5),
            ('ENG', 'English', 'A', 5),
        ]
        
        for code, name, grade, points in subjects:
            ACSSEESubject.objects.create(
                student=cls.student,
                subject_code=code,
                subject_name=name,
                grade=grade,
                points=points
            )
        
        # Create test student with lower grades
        cls.student_low = Student.objects.create(
            candidate_number='S0306-0010',
            full_name='Jane Smith',
            examination_year=2023,
            sex='F'
        )
        
        low_subjects = [
            ('PHY', 'Physics', 'D', 2),
            ('CHE', 'Chemistry', 'D', 2),
            ('BIO', 'Biology', 'D', 2),
            ('MAT', 'Mathematics', 'D', 2),
        ]
        
        for code, name, grade, points in low_subjects:
            ACSSEESubject.objects.create(
                student=cls.student_low,
                subject_code=code,
                subject_name=name,
                grade=grade,
                points=points
            )
        
        # Create institutions
        cls.udsm = Institution.objects.create(
            name='University of Dar es Salaam',
            code='UDSM',
            type='UNI',
            location='Dar es Salaam',
            region='Dar es Salaam',
            website='https://www.udsm.ac.tz',
            admission_portal='https://admission.udsm.ac.tz'
        )
        
        cls.ardhi = Institution.objects.create(
            name='Ardhi University',
            code='ARU',
            type='UNI',
            location='Dar es Salaam',
            region='Dar es Salaam',
            website='https://www.aru.ac.tz',
            admission_portal='https://admission.aru.ac.tz'
        )
        
        cls.ifm = Institution.objects.create(
            name='Institute of Finance Management',
            code='IFM',
            type='INSTITUTE',
            location='Dar es Salaam',
            region='Dar es Salaam'
        )
        
        # Create programmes
        cls.programme_medicine = Programme.objects.create(
            institution=cls.udsm,
            name='Bachelor of Medicine and Surgery',
            code='BMed',
            programme_type='BACHELOR',
            duration=5,
            min_points=17,
            required_subjects=['BIO', 'CHE', 'PHY', 'MAT']
        )
        
        cls.programme_engineering = Programme.objects.create(
            institution=cls.udsm,
            name='Bachelor of Engineering',
            code='BEng',
            programme_type='BACHELOR',
            duration=4,
            min_points=14,
            required_subjects=['MAT', 'PHY', 'CHE']
        )
        
        cls.programme_arts = Programme.objects.create(
            institution=cls.udsm,
            name='Bachelor of Arts',
            code='BA',
            programme_type='BACHELOR',
            duration=3,
            min_points=8,
            required_subjects=[]  # No specific subject requirements
        )
        
        cls.programme_architecture = Programme.objects.create(
            institution=cls.ardhi,
            name='Bachelor of Architecture',
            code='BArch',
            programme_type='BACHELOR',
            duration=5,
            min_points=12,
            required_subjects=['MAT', 'PHY', 'ART']  # ART not taken by student
        )
        
        cls.programme_management = Programme.objects.create(
            institution=cls.ifm,
            name='Bachelor of Business Management',
            code='BBM',
            programme_type='BACHELOR',
            duration=3,
            min_points=10,
            required_subjects=['ECO', 'BAS']  # Economics and Basic Maths
        )
        
        # Create admission links
        AdmissionLink.objects.create(
            institution=cls.udsm,
            programme=cls.programme_medicine,
            url='https://admission.udsm.ac.tz/apply/medicine',
            link_type='PROGRAMME'
        )
        
        AdmissionLink.objects.create(
            institution=cls.udsm,
            programme=cls.programme_engineering,
            url='https://admission.udsm.ac.tz/apply/engineering',
            link_type='PROGRAMME'
        )
        
        cls.engine = CourseRecommendationEngine()
        print("Test data setup complete.")
    
    def test_migration_and_setup(self):
        """Test that migrations and setup worked"""
        # Check students
        self.assertEqual(Student.objects.count(), 2)
        self.assertEqual(ACSSEESubject.objects.count(), 9)
        
        # Check institutions
        self.assertEqual(Institution.objects.count(), 3)
        
        # Check programmes
        self.assertEqual(Programme.objects.count(), 5)
        
        # Check admission links
        self.assertEqual(AdmissionLink.objects.count(), 2)
    
    def test_student_total_points(self):
        """Test student total points calculation"""
        self.assertEqual(self.student.get_total_points(), 24)  # 5+5+4+5+5 = 24
        self.assertEqual(self.student_low.get_total_points(), 8)  # 2+2+2+2 = 8
    
    def test_student_subject_combination(self):
        """Test student subject combination retrieval"""
        subjects = self.student.get_subject_combination()
        self.assertEqual(len(subjects), 5)
        self.assertIn('PHY', subjects)
        self.assertIn('CHE', subjects)
        self.assertIn('BIO', subjects)
        self.assertIn('MAT', subjects)
        self.assertIn('ENG', subjects)
    
    def test_recommendation_generation_high_performer(self):
        """Test recommendations for high-performing student"""
        recommendations = self.engine.generate_recommendations(self.student)
        
        self.assertIn('qualified', recommendations)
        self.assertIn('not_qualified', recommendations)
        self.assertIn('total_qualified', recommendations)
        
        qualified = recommendations['qualified']
        not_qualified = recommendations['not_qualified']
        
        # High performer should qualify for medicine
        medicine_qualified = any(r['name'] == 'Bachelor of Medicine and Surgery' for r in qualified)
        self.assertTrue(medicine_qualified)
        
        # High performer should qualify for engineering
        engineering_qualified = any(r['name'] == 'Bachelor of Engineering' for r in qualified)
        self.assertTrue(engineering_qualified)
        
        # High performer should qualify for arts
        arts_qualified = any(r['name'] == 'Bachelor of Arts' for r in qualified)
        self.assertTrue(arts_qualified)
        
        # Should not qualify for architecture (missing ART)
        architecture_qualified = any(r['name'] == 'Bachelor of Architecture' for r in qualified)
        self.assertFalse(architecture_qualified)
        
        # Should not qualify for management (missing ECO, BAS)
        management_qualified = any(r['name'] == 'Bachelor of Business Management' for r in qualified)
        self.assertFalse(management_qualified)
    
    def test_recommendation_generation_low_performer(self):
        """Test recommendations for low-performing student"""
        recommendations = self.engine.generate_recommendations(self.student_low)
        
        qualified = recommendations['qualified']
        not_qualified = recommendations['not_qualified']
        
        # Low performer should not qualify for medicine
        medicine_qualified = any(r['name'] == 'Bachelor of Medicine and Surgery' for r in qualified)
        self.assertFalse(medicine_qualified)
        
        # Low performer should not qualify for engineering
        engineering_qualified = any(r['name'] == 'Bachelor of Engineering' for r in qualified)
        self.assertFalse(engineering_qualified)
        
        # Low performer might qualify for arts (low points but no subject requirement)
        arts_qualified = any(r['name'] == 'Bachelor of Arts' for r in qualified)
        # Arts requires 8 points, student has 8, so should qualify
        self.assertTrue(arts_qualified)
    
    def test_eligibility_check(self):
        """Test eligibility checking logic"""
        student_subjects = ['PHY', 'CHE', 'BIO', 'MAT', 'ENG']
        total_points = 24
        
        # Test Medicine (requires BIO, CHE, PHY, MAT, min 17)
        programme_data = {
            'required_subjects': ['BIO', 'CHE', 'PHY', 'MAT'],
            'min_points': 17
        }
        result = self.engine._check_eligibility(programme_data, student_subjects, total_points)
        self.assertTrue(result['qualified'])
        self.assertTrue(result['points_met'])
        self.assertTrue(result['subjects_met'])
        self.assertEqual(result['match_score'], 100)
        
        # Test Architecture (requires MAT, PHY, ART, min 12)
        programme_data = {
            'required_subjects': ['MAT', 'PHY', 'ART'],
            'min_points': 12
        }
        result = self.engine._check_eligibility(programme_data, student_subjects, total_points)
        self.assertFalse(result['qualified'])
        self.assertTrue(result['points_met'])
        self.assertFalse(result['subjects_met'])
        self.assertIn('ART', result['missing_subjects'])
        
        # Test with insufficient points
        programme_data = {
            'required_subjects': ['BIO', 'CHE', 'PHY'],
            'min_points': 30  # Higher than student's points
        }
        result = self.engine._check_eligibility(programme_data, student_subjects, total_points)
        self.assertFalse(result['qualified'])
        self.assertFalse(result['points_met'])
        self.assertTrue(result['subjects_met'])
    
    def test_caching_functionality(self):
        """Test that recommendations are cached"""
        from django.core.cache import cache
        cache.clear()
        
        # First call generates recommendations
        first_result = self.engine.generate_recommendations(self.student)
        
        # Second call should use cache
        second_result = self.engine.generate_recommendations(self.student)
        
        # Results should be identical
        self.assertEqual(first_result, second_result)
    
    def test_programme_required_subjects_json(self):
        """Test that required_subjects field works as JSON"""
        # Check that required_subjects is stored as JSON
        self.assertIsInstance(self.programme_medicine.required_subjects, list)
        self.assertEqual(len(self.programme_medicine.required_subjects), 4)
        self.assertIn('BIO', self.programme_medicine.required_subjects)
        self.assertIn('CHE', self.programme_medicine.required_subjects)
        self.assertIn('PHY', self.programme_medicine.required_subjects)
        self.assertIn('MAT', self.programme_medicine.required_subjects)
        
        # Programme with no required subjects
        self.assertEqual(self.programme_arts.required_subjects, [])
    
    def test_student_subject_grades(self):
        """Test student subject grades retrieval"""
        grades = self.student.get_subject_grades()
        self.assertEqual(len(grades), 5)
        self.assertEqual(grades['PHY'], 'A')
        self.assertEqual(grades['CHE'], 'A')
        self.assertEqual(grades['BIO'], 'B')
        self.assertEqual(grades['MAT'], 'A')
        self.assertEqual(grades['ENG'], 'A')
    
    def test_programme_duration_display(self):
        """Test programme duration display"""
        self.assertEqual(self.programme_medicine.get_duration_display(), '5 years')
        self.assertEqual(self.programme_engineering.get_duration_display(), '4 years')
        self.assertEqual(self.programme_arts.get_duration_display(), '3 years')
    
    def test_institution_programme_count(self):
        """Test institution programme count"""
        # UDSM has 3 programmes
        self.assertEqual(self.udsm.get_programme_count(), 3)
        # ARU has 1 programme
        self.assertEqual(self.ardhi.get_programme_count(), 1)
        # IFM has 1 programme
        self.assertEqual(self.ifm.get_programme_count(), 1)
    
    def test_recommendations_with_cache(self):
        """Test that recommendations use cache properly"""
        from django.core.cache import cache
        cache_key = f"course_recs_{self.student.candidate_number}"
        
        # Clear cache
        cache.delete(cache_key)
        
        # First call should generate and cache
        self.engine.generate_recommendations(self.student)
        self.assertIsNotNone(cache.get(cache_key))
        
        # Second call should use cache
        with self.assertLogs(level='INFO') as log:
            self.engine.generate_recommendations(self.student)
            # Should log that cache was used
        
        # Clear cache
        cache.delete(cache_key)
    
    def test_recommendation_model(self):
        """Test Recommendation model"""
        from apps.recommendations.models import Recommendation
        
        # Create a recommendation
        rec = Recommendation.objects.create(
            student=self.student,
            programme=self.programme_medicine,
            institution=self.udsm,
            qualified=True,
            points_score=24,
            subjects_match=True,
            missing_subjects=[],
            relevance_score=95.0
        )
        
        self.assertEqual(rec.student.candidate_number, 'S0306-0005')
        self.assertEqual(rec.programme.name, 'Bachelor of Medicine and Surgery')
        self.assertTrue(rec.qualified)
        self.assertEqual(rec.points_score, 24)
        
        # Test string representation
        self.assertIn('Qualified', str(rec))
    
    def test_get_top_recommendations(self):
        """Test getting top recommendations"""
        top_recs = self.engine.get_top_recommendations(self.student, limit=3)
        
        # Should return at most 3 recommendations
        self.assertLessEqual(len(top_recs), 3)
        
        # Should be sorted by relevance
        if len(top_recs) > 1:
            for i in range(len(top_recs) - 1):
                self.assertGreaterEqual(
                    top_recs[i].get('match_score', 0),
                    top_recs[i+1].get('match_score', 0)
                )
    
    def test_performance_of_recommendation_generation(self):
        """Test performance of recommendation generation"""
        import time
        
        start_time = time.time()
        self.engine.generate_recommendations(self.student)
        end_time = time.time()
        
        # Should complete in under 1 second
        self.assertLess(end_time - start_time, 1.0)


class AdminFunctionalityTest(TestCase):
    """Test admin functionality"""
    
    def setUp(self):
        # Create superuser
        self.admin_user = User.objects.create_superuser(
            username='admin',
            email='admin@example.com',
            password='admin123'
        )
        
        # Create test student
        self.student = Student.objects.create(
            candidate_number='S0306-0005',
            full_name='John Doe',
            examination_year=2023,
            sex='M'
        )
        
        # Create subjects
        subjects = [
            ('PHY', 'Physics', 'A', 5),
            ('CHE', 'Chemistry', 'A', 5),
            ('BIO', 'Biology', 'B', 4),
        ]
        
        for code, name, grade, points in subjects:
            ACSSEESubject.objects.create(
                student=self.student,
                subject_code=code,
                subject_name=name,
                grade=grade,
                points=points
            )
    
    def test_admin_login(self):
        """Test admin login"""
        response = self.client.post('/admin/login/', {
            'username': 'admin',
            'password': 'admin123'
        })
        self.assertEqual(response.status_code, 302)  # Redirect to admin dashboard
    
    def test_admin_student_list(self):
        """Test admin student list view"""
        self.client.login(username='admin', password='admin123')
        response = self.client.get('/admin/students/student/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'John Doe')
        self.assertContains(response, 'S0306-0005')
    
    def test_admin_institution_list(self):
        """Test admin institution list view"""
        from apps.institutions.models import Institution
        
        Institution.objects.create(
            name='Test University',
            code='TU',
            type='UNI'
        )
        
        self.client.login(username='admin', password='admin123')
        response = self.client.get('/admin/institutions/institution/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Test University')
    
    def test_admin_programme_list(self):
        """Test admin programme list view"""
        from apps.institutions.models import Institution, Programme
        
        inst = Institution.objects.create(
            name='Test University',
            code='TU',
            type='UNI'
        )
        
        Programme.objects.create(
            institution=inst,
            name='Test Programme',
            code='TP',
            programme_type='BACHELOR',
            duration=3,
            min_points=10
        )
        
        self.client.login(username='admin', password='admin123')
        response = self.client.get('/admin/institutions/programme/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Test Programme')


class ViewFunctionalityTest(TestCase):
    """Test view functionality"""
    
    def setUp(self):
        self.student = Student.objects.create(
            candidate_number='S0306-0005',
            full_name='John Doe',
            examination_year=2023,
            sex='M'
        )
        
        subjects = [
            ('PHY', 'Physics', 'A', 5),
            ('CHE', 'Chemistry', 'A', 5),
        ]
        
        for code, name, grade, points in subjects:
            ACSSEESubject.objects.create(
                student=self.student,
                subject_code=code,
                subject_name=name,
                grade=grade,
                points=points
            )
    
    def test_index_page(self):
        """Test index page loads"""
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'UniCourseFinder')
    
    def test_search_page(self):
        """Test search page loads"""
        response = self.client.get('/search/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Search')
        self.assertContains(response, 'Candidate Number')
    
    def test_student_search_post(self):
        """Test student search POST request"""
        response = self.client.post('/search/', {
            'candidate_number': 'S0306-0005',
            'examination_year': '2023'
        })
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'John Doe')
        self.assertContains(response, 'Physics')
        self.assertContains(response, 'Chemistry')
    
    def test_student_profile(self):
        """Test student profile page"""
        response = self.client.get(f'/profile/{self.student.candidate_number}/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'John Doe')
        self.assertContains(response, 'S0306-0005')


if __name__ == '__main__':
    import unittest
    unittest.main()