from django.test import TestCase, Client
from django.test.utils import override_settings
from django.urls import reverse
from django.db import connection
from django.db import reset_queries
from .models import Event, Occurrence
from .services.event_service import EventService


class EventModelTests(TestCase):
    """Tests for Event model functionality."""
    
    def setUp(self):
        """Set up test data."""
        self.project = Event.objects.create(
            name='Test Project',
            event_type='project'
        )
        self.site = Event.objects.create(
            name='Test Site',
            event_type='site',
            parent=self.project,
            footprintWKT='POINT(-3.0 50.0)'
        )
        self.visit = Event.objects.create(
            name='Test Visit',
            event_type='visit',
            parent=self.site
        )
    
    def test_event_str(self):
        """Test Event string representation."""
        self.assertEqual(str(self.project), 'Test Project (Project)')
        self.assertEqual(str(self.site), 'Test Site (Site)')
    
    def test_event_hierarchy(self):
        """Test event parent-child relationships."""
        self.assertEqual(self.site.parent, self.project)
        self.assertIn(self.site, self.project.children.all())
        self.assertEqual(self.visit.parent, self.site)
        self.assertIn(self.visit, self.site.children.all())


class ViewTests(TestCase):
    """Tests for eventhub views."""
    
    def setUp(self):
        """Set up test data."""
        self.client = Client()
        self.project = Event.objects.create(
            name='Test Project',
            event_type='project'
        )
        self.site1 = Event.objects.create(
            name='Site 1',
            event_type='site',
            parent=self.project,
            footprintWKT='POINT(-3.0 50.0)'
        )
        self.site2 = Event.objects.create(
            name='Site 2',
            event_type='site',
            parent=self.project,
            footprintWKT='POINT(-3.1 50.1)'
        )
        self.visit = Event.objects.create(
            name='Visit 1',
            event_type='visit',
            parent=self.site1
        )
        self.occurrence = Occurrence.objects.create(
            event=self.visit,
            taxon_name='Testus taxonus',
            abundance='Common'
        )
    
    def test_event_list_view(self):
        """Test event list view renders correctly (defaults to projects)."""
        response = self.client.get(reverse('eventhub:event_list'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Test Project')
    
    def test_event_list_context_has_nodes(self):
        """Test event list view includes nodes in context."""
        response = self.client.get(reverse('eventhub:event_list'))
        self.assertEqual(response.status_code, 200)
        self.assertIn('nodes', response.context)
        nodes = response.context['nodes']
        self.assertEqual(len(nodes), 1)
        self.assertEqual(nodes[0]['id'], self.project.id)
        self.assertEqual(nodes[0]['label'], 'Test Project')
        self.assertEqual(nodes[0]['children_count'], 2)  # Two sites
    
    def test_event_geometry_json(self):
        """Test event geometry JSON endpoint."""
        response = self.client.get(reverse('eventhub:event_geometry', args=[self.site1.id]))
        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertEqual(data['event_id'], self.site1.id)
        self.assertEqual(data['event_name'], 'Site 1')
        self.assertEqual(data['event_type'], 'site')
        self.assertEqual(data['project_id'], self.project.id)
        self.assertEqual(len(data['geometries']), 1)
        self.assertEqual(data['geometries'][0]['wkt'], 'POINT(-3.0 50.0)')
    
    def test_event_geometry_project(self):
        """Test event geometry JSON endpoint for project."""
        response = self.client.get(reverse('eventhub:event_geometry', args=[self.project.id]))
        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertEqual(data['event_type'], 'project')
        self.assertEqual(data['geometries'], [])
    
    def test_event_children_geometries_json(self):
        """Test event children geometries JSON endpoint."""
        response = self.client.get(reverse('eventhub:event_children_geometries', args=[self.project.id]))
        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertEqual(data['event_id'], self.project.id)
        self.assertEqual(len(data['children']), 2)
        self.assertEqual(data['children'][0]['id'], self.site1.id)
        self.assertEqual(data['children'][0]['name'], 'Site 1')
        self.assertEqual(data['children'][0]['footprintWKT'], 'POINT(-3.0 50.0)')
    
    def test_event_children_geometries_not_project(self):
        """Test event children geometries endpoint rejects non-project events."""
        response = self.client.get(reverse('eventhub:event_children_geometries', args=[self.site1.id]))
        self.assertEqual(response.status_code, 400)
        data = response.json()
        self.assertIn('error', data)
    
    def test_event_children_htmx(self):
        """Test event children HTMX endpoint."""
        response = self.client.get(
            reverse('eventhub:event_children', args=[self.project.id]),
            HTTP_HX_REQUEST='true'
        )
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Site 1')
        self.assertContains(response, 'Site 2')
        self.assertIn('nodes', response.context)
        nodes = response.context['nodes']
        self.assertEqual(len(nodes), 2)
        self.assertEqual(nodes[0]['children_count'], 1)  # Site 1 has one visit
    
    def test_event_details_htmx(self):
        """Test event details HTMX endpoint."""
        response = self.client.get(
            reverse('eventhub:event_details', args=[self.visit.id]),
            HTTP_HX_REQUEST='true'
        )
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Visit 1')
        self.assertIn('event', response.context)
        self.assertEqual(response.context['event'].id, self.visit.id)
        self.assertEqual(response.context['direct_occurrences_count'], 1)
        self.assertEqual(response.context['direct_taxa_count'], 1)
    
    def test_event_details_descendant_statistics(self):
        """Test event details includes descendant statistics."""
        # Add another occurrence to a child event
        site_occurrence = Occurrence.objects.create(
            event=self.site1,
            taxon_name='Siteus taxonus'
        )
        
        response = self.client.get(
            reverse('eventhub:event_details', args=[self.project.id]),
            HTTP_HX_REQUEST='true'
        )
        self.assertEqual(response.status_code, 200)
        # Project has no direct occurrences
        self.assertEqual(response.context['direct_occurrences_count'], 0)
        # But has occurrences in descendants
        self.assertGreater(response.context['child_occurrences_count'], 0)
    
    def test_get_all_descendant_ids_iterative(self):
        """Test _get_all_descendant_ids uses iterative approach."""
        from eventhub.views import _get_all_descendant_ids
        
        # Create a deeper hierarchy
        child1 = Event.objects.create(
            name='Child 1',
            event_type='sample',
            parent=self.visit
        )
        child2 = Event.objects.create(
            name='Child 2',
            event_type='sample',
            parent=self.visit
        )
        grandchild = Event.objects.create(
            name='Grandchild',
            event_type='note',
            parent=child1
        )
        
        descendant_ids = _get_all_descendant_ids(self.project)
        # Should include all descendants: site1, site2, visit, child1, child2, grandchild
        expected_ids = {self.site1.id, self.site2.id, self.visit.id, child1.id, child2.id, grandchild.id}
        self.assertEqual(descendant_ids, expected_ids)
    
    def test_get_all_descendant_ids_empty(self):
        """Test _get_all_descendant_ids returns empty set for leaf node."""
        from eventhub.views import _get_all_descendant_ids
        
        # Create a leaf event with no children
        leaf = Event.objects.create(
            name='Leaf',
            event_type='sample',
            parent=self.visit
        )
        
        descendant_ids = _get_all_descendant_ids(leaf)
        self.assertEqual(descendant_ids, set())
    
    def test_get_project_id(self):
        """Test _get_project_id helper function."""
        from eventhub.views import _get_project_id
        
        self.assertEqual(_get_project_id(self.project), self.project.id)
        self.assertEqual(_get_project_id(self.site1), self.project.id)
        self.assertEqual(_get_project_id(self.visit), self.project.id)


class EventServiceTests(TestCase):
    """Tests for EventService business logic."""
    
    def setUp(self):
        """Set up test data."""
        self.project = Event.objects.create(
            name='Test Project',
            event_type='project'
        )
        self.site = Event.objects.create(
            name='Test Site',
            event_type='site',
            parent=self.project,
            footprintWKT='POINT(-3.0 50.0)'
        )
        self.visit = Event.objects.create(
            name='Test Visit',
            event_type='visit',
            parent=self.site
        )
        self.occurrence1 = Occurrence.objects.create(
            event=self.visit,
            taxon_name='Testus taxonus',
            abundance='Common'
        )
        self.occurrence2 = Occurrence.objects.create(
            event=self.visit,
            taxon_name='Testus taxonus',
            abundance='Rare'
        )
        self.occurrence3 = Occurrence.objects.create(
            event=self.site,
            taxon_name='Siteus taxonus',
            abundance='Common'
        )
    
    def test_get_occurrences_for_event_current_level(self):
        """Test getting occurrences at current level only."""
        occurrences = EventService.get_occurrences_for_event(self.visit, include_descendants=False)
        occurrence_list = list(occurrences)
        self.assertEqual(len(occurrence_list), 2)
        self.assertIn(self.occurrence1, occurrence_list)
        self.assertIn(self.occurrence2, occurrence_list)
        self.assertNotIn(self.occurrence3, occurrence_list)
    
    def test_get_occurrences_for_event_with_descendants(self):
        """Test getting occurrences including descendants."""
        occurrences = EventService.get_occurrences_for_event(self.site, include_descendants=True)
        occurrence_list = list(occurrences)
        self.assertEqual(len(occurrence_list), 3)
        self.assertIn(self.occurrence1, occurrence_list)
        self.assertIn(self.occurrence2, occurrence_list)
        self.assertIn(self.occurrence3, occurrence_list)
    
    def test_get_taxa_for_occurrences(self):
        """Test getting distinct taxa from occurrences."""
        occurrences = EventService.get_occurrences_for_event(self.visit, include_descendants=False)
        taxa = EventService.get_taxa_for_occurrences(occurrences)
        taxa_list = list(taxa)
        
        self.assertEqual(len(taxa_list), 1)
        self.assertEqual(taxa_list[0]['taxon_name'], 'Testus taxonus')
        self.assertEqual(taxa_list[0]['occurrence_count'], 2)
    
    def test_get_statistics(self):
        """Test getting event statistics."""
        stats = EventService.get_statistics(self.site)
        
        # Site has 1 direct occurrence
        self.assertEqual(stats.direct_occurrences_count, 1)
        self.assertEqual(stats.direct_taxa_count, 1)
        
        # Site has 2 occurrences in descendants (visit)
        self.assertEqual(stats.child_occurrences_count, 2)
        self.assertEqual(stats.child_taxa_count, 1)
        
        # Site has 1 child (visit)
        self.assertEqual(stats.children_count, 1)
    
    def test_get_statistics_no_descendants(self):
        """Test statistics for event with no descendants."""
        stats = EventService.get_statistics(self.visit)
        
        self.assertEqual(stats.direct_occurrences_count, 2)
        self.assertEqual(stats.direct_taxa_count, 1)
        self.assertEqual(stats.child_occurrences_count, 0)
        self.assertEqual(stats.child_taxa_count, 0)
        self.assertEqual(stats.children_count, 0)
    
    def test_get_statistics_query_efficiency(self):
        """Test that get_statistics uses aggregate to avoid multiple QuerySet evaluations."""
        reset_queries()
        
        stats = EventService.get_statistics(self.site)
        
        # Should use aggregate() to get both counts in one query
        # Check that we're not doing multiple .count() calls on the same queryset
        queries = [q['sql'] for q in connection.queries]
        
        # Count SQL queries - should be efficient (not multiple counts on same queryset)
        # We expect: 1 for direct stats aggregate, 1 for children count, 1 for descendant stats aggregate
        # Plus any queries for get_descendants() which uses path lookup
        self.assertLess(len(queries), 10, "Too many queries - check for N+1 or multiple QuerySet evaluations")
    
    def test_event_breadcrumb(self):
        """Test Event.get_breadcrumb method."""
        breadcrumb = self.visit.get_breadcrumb()
        
        self.assertEqual(len(breadcrumb), 3)
        self.assertEqual(breadcrumb[0]['name'], 'Test Project')
        self.assertEqual(breadcrumb[0]['event_type'], 'project')
        self.assertEqual(breadcrumb[1]['name'], 'Test Site')
        self.assertEqual(breadcrumb[1]['event_type'], 'site')
        self.assertEqual(breadcrumb[2]['name'], 'Test Visit')
        self.assertEqual(breadcrumb[2]['event_type'], 'visit')
    
    def test_event_breadcrumb_root(self):
        """Test breadcrumb for root event."""
        breadcrumb = self.project.get_breadcrumb()
        
        self.assertEqual(len(breadcrumb), 1)
        self.assertEqual(breadcrumb[0]['name'], 'Test Project')
    
    def test_event_details_view_query_count(self):
        """Test that event_details view doesn't have N+1 query issues."""
        from django.test import Client
        client = Client()
        
        reset_queries()
        
        response = client.get(
            reverse('eventhub:event_details', args=[self.site.id]),
            HTTP_HX_REQUEST='true'
        )
        
        self.assertEqual(response.status_code, 200)
        
        # Check query count - should be reasonable (not N+1)
        queries = connection.queries
        self.assertLess(len(queries), 15, "Too many queries in event_details view - check for N+1 issues")
    
    def test_get_descendants_path_format(self):
        """Test that get_descendants works with the actual path format in database."""
        # Ensure paths are set (they should be from save() method)
        self.project.save()
        self.site.save()
        self.visit.save()
        
        # Refresh from DB to get actual path values
        self.project.refresh_from_db()
        self.site.refresh_from_db()
        self.visit.refresh_from_db()
        
        # Debug: print actual path values
        print(f"Project (id={self.project.id}) path: '{self.project.path}'")
        print(f"Site (id={self.site.id}) path: '{self.site.path}'")
        print(f"Visit (id={self.visit.id}) path: '{self.visit.path}'")
        
        # Test get_descendants
        project_descendants = list(self.project.get_descendants())
        print(f"Project descendants: {[d.id for d in project_descendants]}")
        
        # Project should have Site and Visit as descendants
        self.assertIn(self.site, project_descendants)
        self.assertIn(self.visit, project_descendants)
