import tempfile
from datetime import timedelta

import pytest
from birds.importer import (
    import_birding_csv,
    parse_bool,
    parse_coords,
    parse_duration,
    parse_int,
    parse_timestamp,
)
from birds.models import Bird, BirdingLocation, BirdingCSVImport
from scrobbles.models import Scrobble


class TestParserHelpers:
    def test_parse_duration(self):
        assert parse_duration("9 minute(s)") == 9
        assert parse_duration("120 minute(s)") == 120
        assert parse_duration("") is None
        assert parse_duration(None) is None
        assert parse_duration("not a duration") is None

    def test_parse_coords(self):
        loc = "Some Place, US (44.384, -68.805)"
        lat, lon = parse_coords(loc)
        assert lat == 44.384
        assert lon == -68.805

    def test_parse_coords_no_match(self):
        loc = "Some Place, US"
        lat, lon = parse_coords(loc)
        assert lat is None
        assert lon is None

    def test_parse_timestamp(self):
        dt = parse_timestamp("May 10, 2026", "4:15 PM")
        assert dt is not None
        assert dt.year == 2026
        assert dt.month == 5
        assert dt.day == 10
        assert dt.hour == 16
        assert dt.minute == 15

    def test_parse_timestamp_no_time(self):
        dt = parse_timestamp("May 10, 2026", "")
        assert dt is not None
        assert dt.year == 2026

    def test_parse_timestamp_invalid(self):
        assert parse_timestamp("not a date", "") is None

    def test_parse_bool(self):
        assert parse_bool("true") is True
        assert parse_bool("True") is True
        assert parse_bool("yes") is True
        assert parse_bool("1") is True
        assert parse_bool("false") is False
        assert parse_bool("") is None
        assert parse_bool(None) is None

    def test_parse_int(self):
        assert parse_int("42") == 42
        assert parse_int("") is None
        assert parse_int(None) is None
        assert parse_int("not a number") is None


class TestImportBirdingCSV:
    def test_import_creates_birds(self, user, birding_csv_file):
        import_birding_csv(birding_csv_file, user.id)
        assert Bird.objects.filter(common_name="Canada Goose").exists()
        assert Bird.objects.filter(common_name="Northern Cardinal").exists()

    def test_import_creates_location(self, user, birding_csv_file):
        import_birding_csv(birding_csv_file, user.id)
        assert BirdingLocation.objects.filter(title="Test Park").exists()

    def test_import_creates_scrobble(self, user, birding_csv_file):
        import_birding_csv(birding_csv_file, user.id)
        assert Scrobble.objects.filter(
            source="Birding CSV Import"
        ).count() == 1

    def test_import_logdata_fields(self, user, birding_csv_file):
        import_birding_csv(birding_csv_file, user.id)
        scrobble = Scrobble.objects.filter(source="Birding CSV Import").first()
        log = scrobble.log
        assert log["duration_minutes"] == 9
        assert log["observation_type"] == "Stationary"
        assert log["party_size"] == 4
        assert log["complete_checklist"] is True
        assert len(log["birds"]) == 2

    def test_import_sighting_details(self, user, birding_csv_file):
        import_birding_csv(birding_csv_file, user.id)
        scrobble = Scrobble.objects.filter(source="Birding CSV Import").first()
        birds = scrobble.log["birds"]
        cardinal = next(b for b in birds if b["quantity"] == 2)
        assert cardinal["sighting_notes"] == "At the feeder"

    def test_import_idempotent(self, user, birding_csv_file):
        import_birding_csv(birding_csv_file, user.id)
        import_birding_csv(birding_csv_file, user.id)
        assert Scrobble.objects.filter(
            source="Birding CSV Import"
        ).count() == 1

    def test_import_bird_quantities(self, user, birding_csv_file):
        import_birding_csv(birding_csv_file, user.id)
        scrobble = Scrobble.objects.filter(source="Birding CSV Import").first()
        birds = scrobble.log["birds"]
        goose = next(b for b in birds if b["quantity"] == 6)
        assert goose is not None

    def test_import_sets_stop_timestamp(self, user, birding_csv_file):
        import_birding_csv(birding_csv_file, user.id)
        scrobble = Scrobble.objects.filter(source="Birding CSV Import").first()
        assert scrobble.stop_timestamp is not None
        expected = scrobble.timestamp + timedelta(minutes=9)
        assert scrobble.stop_timestamp == expected


class TestBirdingCSVImportModel:
    def test_create_import_model(self, db, user):
        imp = BirdingCSVImport.objects.create(user=user)
        assert imp.uuid is not None
        assert imp.import_type == "Birding CSV"
        assert "Birding" in str(imp)

    def test_record_error(self, db, user):
        imp = BirdingCSVImport.objects.create(user=user)
        assert imp.error_log is None
        imp.record_error("test error")
        imp.refresh_from_db()
        assert imp.error_log is not None
        assert "test error" in imp.error_log

    def test_record_error_appends(self, db, user):
        imp = BirdingCSVImport.objects.create(user=user)
        imp.record_error("first error")
        imp.record_error("second error")
        imp.refresh_from_db()
        assert imp.error_log.count("\n") == 1
        assert "first error" in imp.error_log
        assert "second error" in imp.error_log

    @pytest.mark.django_db(transaction=True)
    def test_process_via_model(self, user, birding_csv_file):
        imp = BirdingCSVImport.objects.create(user=user)
        with open(birding_csv_file, "rb") as f:
            imp.csv_file.save("test.csv", f, save=True)
        imp.process()
        imp.refresh_from_db()
        assert imp.process_count == 1
        assert imp.processed_finished is not None

    def test_record_error_on_bad_csv(self, user, db):
        content = """Species,Count,Location,Observation Type,Observation Date,Start Time,Duration,Distance,Area,Party Size,Complete Checklist,# of species,Details
Canada Goose,6,Test Park,Stationary,"Bad Date",4:15 PM,9 minute(s),,,4,true,4 species,
"""
        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".csv", delete=False, encoding="utf-8"
        ) as f:
            f.write(content)
            file_path = f.name

        errors = []
        scrobbles = import_birding_csv(file_path, user.id, record_error=errors.append)
        assert len(scrobbles) == 0
        assert len(errors) == 1
        assert "Could not parse date/time" in errors[0]

    def test_record_error_on_bad_location(self, user, db):
        content = """Species,Count,Location,Observation Type,Observation Date,Start Time,Duration,Distance,Area,Party Size,Complete Checklist,# of species,Details
Canada Goose,6,,Stationary,"May 10, 2026",4:15 PM,9 minute(s),,,4,true,4 species,
"""
        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".csv", delete=False, encoding="utf-8"
        ) as f:
            f.write(content)
            file_path = f.name

        errors = []
        scrobbles = import_birding_csv(file_path, user.id, record_error=errors.append)
        assert len(scrobbles) == 0
        assert len(errors) == 1
        assert "Skipping rows with no location" in errors[0]
