import os
import tempfile
from datetime import timedelta

import pytest
from django.contrib.auth import get_user_model
from django.core.files import File

from locations.models import GeoLocation
from scrobbles.importers.trail_gpx import (
    compute_trail_stats,
    find_route_waypoint,
    import_trail_gpx,
    parse_trackpoints,
)
from scrobbles.models import Scrobble, TrailGPXImport
from trails.models import Trail, TrailLogData

User = get_user_model()

SAMPLE_GPX = os.path.join(
    os.path.dirname(__file__), "..", "..", "data", "sample_trail.gpx"
)


@pytest.fixture
def user(db):
    return User.objects.create(email="trailblazer@example.com")


@pytest.fixture
def sample_gpx_path():
    return SAMPLE_GPX


class TestParseTrackpoints:
    def test_parses_gpx(self, sample_gpx_path):
        result = parse_trackpoints(sample_gpx_path)
        points = result["points"]
        assert len(points) == 837
        assert result["name"] == "Morning Run ⛅"
        assert result["description"] == "Run"
        lat, lon, ele, t = points[0]
        assert round(lat, 6) == 34.190598
        assert round(lon, 6) == -118.844015
        assert ele == 305.3
        assert t is not None

    def test_first_and_last_times(self, sample_gpx_path):
        result = parse_trackpoints(sample_gpx_path)
        points = result["points"]
        first_time = points[0][3]
        last_time = points[-1][3]
        duration = (last_time - first_time).total_seconds()
        assert duration == pytest.approx(3770, abs=5)

    def test_gpx_extra_metadata(self, sample_gpx_path):
        result = parse_trackpoints(sample_gpx_path)
        extra = result["extra"]
        assert extra["avg_heartrate"] == 159
        assert extra["max_heartrate"] == 183
        assert extra["avg_speed_kmh"] == pytest.approx(9.82, abs=0.1)
        assert extra["activity_type"] == "Run"
        assert extra["moving_time_seconds"] == 3008
        assert extra["total_elevation_gain_m"] == 246.4


class TestImportTrailGPX:
    def test_creates_trail(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        assert Trail.objects.filter(title="Morning Run ⛅").exists()

    def test_creates_geolocation(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        assert GeoLocation.objects.filter(lat=34.190598, lon=-118.844015).exists()

    def test_sets_trailhead(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        trail = Trail.objects.filter(title="Morning Run ⛅").first()
        assert trail.trailhead_location is not None
        assert round(trail.trailhead_location.lat, 6) == 34.190598

    def test_creates_scrobble(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        assert Scrobble.objects.filter(source="GPX Import").count() == 1

    def test_scrobble_timestamps(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        scrobble = Scrobble.objects.filter(source="GPX Import").first()
        assert scrobble.timestamp.isoformat().startswith("2022-06-05T13:55:09")
        assert scrobble.stop_timestamp.isoformat().startswith("2022-06-05T14:57:59")
        assert scrobble.media_type == Scrobble.MediaType.TRAIL

    def test_scrobble_has_trail_fk(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        scrobble = Scrobble.objects.filter(source="GPX Import").first()
        assert scrobble.trail is not None
        assert scrobble.trail.title == "Morning Run ⛅"

    def test_scrobble_has_gpx_file(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        scrobble = Scrobble.objects.filter(source="GPX Import").first()
        assert scrobble.gpx_file
        assert scrobble.gpx_file.name.endswith(".gpx")

    def test_lookup_existing_trail_by_trailhead(self, user, sample_gpx_path):
        geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
        trail = Trail.objects.create(title="Existing Trail", trailhead_location=geo)
        import_trail_gpx(sample_gpx_path, user.id)
        scrobble = Scrobble.objects.filter(source="GPX Import").first()
        assert scrobble.trail.id == trail.id

    def test_dedup(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        import_trail_gpx(sample_gpx_path, user.id)
        assert Scrobble.objects.filter(source="GPX Import").count() == 1

    def test_scrobble_log_has_stats(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        scrobble = Scrobble.objects.filter(source="GPX Import").first()
        log = scrobble.log
        assert log["distance_km"] == pytest.approx(8.2, abs=0.2)
        assert log["elevation_gain_m"] == pytest.approx(260, abs=20)
        assert log["moving_time_seconds"] == pytest.approx(3770, abs=10)
        assert log["avg_speed_kmh"] == pytest.approx(7.8, abs=0.5)
        assert log["description"] == "Run"

    def test_scrobble_playback_position(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        scrobble = Scrobble.objects.filter(source="GPX Import").first()
        assert scrobble.playback_position_seconds == pytest.approx(3770, abs=5)

    def test_scrobble_has_timezone(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        scrobble = Scrobble.objects.filter(source="GPX Import").first()
        assert scrobble.timezone is not None
        assert isinstance(scrobble.timezone, str)

    def test_scrobble_log_extra_metadata(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        scrobble = Scrobble.objects.filter(source="GPX Import").first()
        log = scrobble.log
        assert log["avg_heartrate"] == 159
        assert log["max_heartrate"] == 183
        assert log["activity_type"] == "Run"

    def test_scrobble_log_no_calories_in_gpx(self, user, sample_gpx_path):
        import_trail_gpx(sample_gpx_path, user.id)
        scrobble = Scrobble.objects.filter(source="GPX Import").first()
        assert scrobble.log.get("calories") is None


class TestComputeTrailStats:
    def test_computes_distance_and_elevation(self, sample_gpx_path):
        result = parse_trackpoints(sample_gpx_path)
        stats = compute_trail_stats(result["points"])
        assert stats["distance_km"] == pytest.approx(8.2, abs=0.2)
        assert stats["elevation_gain_m"] == pytest.approx(260, abs=20)
        assert stats["moving_time_seconds"] == pytest.approx(3770, abs=10)
        assert stats["avg_speed_kmh"] == pytest.approx(7.8, abs=0.5)


class TestTrailGPXImportModel:
    def test_create_import_model(self, db, user, sample_gpx_path):
        imp = TrailGPXImport.objects.create(
            user=user,
            original_filename="test_trail.gpx",
        )
        assert imp.uuid is not None
        assert imp.import_type == "Trail GPX"

    @pytest.mark.django_db(transaction=True)
    def test_process_via_model(self, user, sample_gpx_path):
        imp = TrailGPXImport.objects.create(
            user=user,
            original_filename="Morning Run.gpx",
        )
        with open(sample_gpx_path, "rb") as f:
            imp.gpx_file.save("Morning Run.gpx", File(f), save=True)
        imp.process()
        imp.refresh_from_db()
        assert imp.process_count == 1
        assert imp.processed_finished is not None


class TestFindRouteWaypoint:
    def test_returns_halfway_point(self, sample_gpx_path):
        result = parse_trackpoints(sample_gpx_path)
        pt = find_route_waypoint(result["points"])
        assert pt is not None
        lat, lon = pt
        assert lat == pytest.approx(34.177853, abs=0.001)
        assert lon == pytest.approx(-118.829944, abs=0.001)

    def test_returns_last_point_for_short_track(self):
        points = [(34.0, -118.0, None, None), (34.001, -118.001, None, None)]
        pt = find_route_waypoint(points)
        assert pt == (34.001, -118.001)

    def test_returns_none_for_empty_points(self):
        assert find_route_waypoint([]) is None


class TestFindByTrailhead:
    def test_exact_match(self, db):
        geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
        trail = Trail.objects.create(title="Test Trail", trailhead_location=geo)
        found = Trail.find_by_trailhead(34.190598, -118.844015)
        assert found == trail

    def test_within_tolerance(self, db):
        geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
        trail = Trail.objects.create(title="Nearby Trail", trailhead_location=geo)
        found = Trail.find_by_trailhead(34.191000, -118.844000, tolerance_m=100)
        assert found == trail

    def test_beyond_tolerance(self, db):
        geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
        Trail.objects.create(title="Far Trail", trailhead_location=geo)
        found = Trail.find_by_trailhead(34.200000, -118.850000, tolerance_m=50)
        assert found is None

    def test_no_trailhead_returns_none(self, db):
        Trail.objects.create(title="No Location")
        found = Trail.find_by_trailhead(34.190598, -118.844015)
        assert found is None

    def test_same_trailhead_same_route_matches(self, db):
        geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
        trail = Trail.objects.create(
            title="Same Route Trail",
            trailhead_location=geo,
            route_lat=34.192167,
            route_lon=-118.843143,
        )
        found = Trail.find_by_trailhead(
            34.190598, -118.844015,
            route_lat=34.192167, route_lon=-118.843143,
            tolerance_m=100,
        )
        assert found == trail

    def test_same_trailhead_different_route_does_not_match(self, db):
        geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
        Trail.objects.create(
            title="Different Route Trail",
            trailhead_location=geo,
            route_lat=34.200000,
            route_lon=-118.850000,
        )
        found = Trail.find_by_trailhead(
            34.190598, -118.844015,
            route_lat=34.192167, route_lon=-118.843143,
            tolerance_m=100,
        )
        assert found is None

    def test_legacy_trail_without_route_still_matches(self, db):
        geo = GeoLocation.objects.create(lat=34.190598, lon=-118.844015)
        trail = Trail.objects.create(
            title="Legacy Trail",
            trailhead_location=geo,
        )
        found = Trail.find_by_trailhead(
            34.190598, -118.844015,
            route_lat=34.192167, route_lon=-118.843143,
            tolerance_m=100,
        )
        assert found == trail


class TestFindOrCreate:
    def test_find_existing(self, db):
        Trail.objects.create(title="Existing Trail")
        trail = Trail.find_or_create("Existing Trail")
        assert trail.title == "Existing Trail"

    def test_create_new(self, db):
        trail = Trail.find_or_create("New Trail")
        assert trail.title == "New Trail"
        assert Trail.objects.count() == 1
