package web
import (
"database/sql"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/martinlehoux/biking_home/internal/ridetest"
"github.com/martinlehoux/biking_home/official_climb"
"github.com/martinlehoux/biking_home/ride"
"github.com/martinlehoux/biking_home/rideanalysis"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func recordFixtureAttempts(t *testing.T, server *Server, rideID int64, climb official_climb.OfficialClimb) {
t.Helper()
parsed, _ := testRideForClimb(t)
analysis := rideanalysis.Analyze(parsed, nil, []official_climb.OfficialClimb{climb}, official_climb.DefaultMatchPolicy())
server.recordOfficialClimbAttempts(rideID, parsed, analysis)
}
func storeRideFixtureFile(t *testing.T, db *sql.DB, rideID int64) {
t.Helper()
parsed, _ := testRideForClimb(t)
fitPath := filepath.Join(t.TempDir(), "ride.fit")
fitFile, err := os.Create(fitPath)
require.NoError(t, err)
require.NoError(t, ride.EncodeFIT(parsed, fitFile))
require.NoError(t, fitFile.Close())
_, err = db.Exec("UPDATE rides SET fit_path = ? WHERE id = ?", fitPath, rideID)
require.NoError(t, err)
}
func TestHandleClimbRendersAttemptsAndProfile(t *testing.T) {
server, db := newWebTestServer(t)
_, climbFixture := testRideForClimb(t)
created, err := official_climb.Create(db, climbFixture)
require.NoError(t, err)
rideID := insertTestRide(t, db)
storeRideFixtureFile(t, db, rideID)
recordFixtureAttempts(t, server, rideID, created)
recorder := httptest.NewRecorder()
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, climbDetailURL(created.ID), nil))
require.Equal(t, http.StatusOK, recorder.Code)
body := recorder.Body.String()
assert.Contains(t, body, "Col de Test")
assert.Contains(t, body, "climb-profile-data")
assert.Contains(t, body, "Test Ride")
// A ride without a power meter renders a dash, not a fabricated value.
assert.Contains(t, body, `
- | `)
}
func TestHandleClimbRendersAttemptPoints(t *testing.T) {
server, db := newWebTestServer(t)
parsed := ridetest.MustRide("1km at 14.7% w=250 | 1km at -6.7% w=150", ridetest.StartCoord(43.61, 5.42), ridetest.StartElevation(300), ridetest.Sampling(1000), ridetest.StartTime(testRideStart))
created, err := official_climb.Create(db, official_climb.OfficialClimb{Name: "Col de Test", StartCoord: parsed.Coords[0], EndCoord: parsed.Coords[1], Sample: parsed.Sample(0, 1)})
require.NoError(t, err)
rideID := insertTestRide(t, db)
analysis := rideanalysis.Analyze(parsed, nil, []official_climb.OfficialClimb{created}, official_climb.DefaultMatchPolicy())
server.recordOfficialClimbAttempts(rideID, parsed, analysis)
recorder := httptest.NewRecorder()
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, climbDetailURL(created.ID), nil))
require.Equal(t, http.StatusOK, recorder.Code)
body := recorder.Body.String()
assert.Contains(t, body, `id="climb-attempt-points"`)
// Mean of the climb interval power samples: 250, 250 W.
assert.Contains(t, body, `"averagePowerW":250`)
assert.Contains(t, body, `"url":"/rides/`)
assert.Contains(t, body, "Avg power")
assert.Contains(t, body, "250 W")
}
func TestClimbAttemptPointsSkipAttemptsWithoutPower(t *testing.T) {
power := 235.5
views := []ClimbAttemptView{
{RideID: 7, RideName: "Powered", Date: "01 Sep 2026, 10:00", DurationS: 1110, AveragePowerW: &power},
{RideID: 8, RideName: "No meter", Date: "02 Sep 2026, 10:00", DurationS: 1200},
}
points := climbAttemptPoints(views)
require.Len(t, points, 1)
assert.Equal(t, ClimbAttemptPoint{DurationS: 1110, AveragePowerW: 235.5, Date: "01 Sep 2026, 10:00", RideName: "Powered", URL: "/rides/7"}, points[0])
}
func TestHandleClimbNotFound(t *testing.T) {
server, _ := newWebTestServer(t)
recorder := httptest.NewRecorder()
server.Handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/climbs/999", nil))
assert.Equal(t, http.StatusNotFound, recorder.Code)
}
func TestBuildClimbProfileMediansAcrossRides(t *testing.T) {
server, db := newWebTestServer(t)
_, climbFixture := testRideForClimb(t)
created, err := official_climb.Create(db, climbFixture)
require.NoError(t, err)
rideID := insertTestRide(t, db)
storeRideFixtureFile(t, db, rideID)
parsed, _ := testRideForClimb(t)
analysis := rideanalysis.Analyze(parsed, nil, []official_climb.OfficialClimb{created}, official_climb.DefaultMatchPolicy())
attempts := rideanalysis.Attempts(rideID, analysis, parsed)
profile := server.buildClimbProfile(created, official_climb.AggregateAttempts(attempts), attempts)
assert.InDelta(t, 1.0, profile.DistanceKm, 0.01)
require.Len(t, profile.Elevations, 50)
assert.InDelta(t, 300, profile.Elevations[0], 1)
assert.InDelta(t, 447, profile.Elevations[49], 2)
}
func TestBuildClimbProfileRouteKeepsRecordedPoints(t *testing.T) {
server, db := newWebTestServer(t)
parsed, climbFixture := testRideForClimb(t)
created, err := official_climb.Create(db, climbFixture)
require.NoError(t, err)
rideID := insertTestRide(t, db)
storeRideFixtureFile(t, db, rideID)
analysis := rideanalysis.Analyze(parsed, nil, []official_climb.OfficialClimb{created}, official_climb.DefaultMatchPolicy())
attempts := rideanalysis.Attempts(rideID, analysis, parsed)
profile := server.buildClimbProfile(created, official_climb.AggregateAttempts(attempts), attempts)
require.NotEmpty(t, profile.Route)
// Full recorded precision: every matched segment point is present, not a fixed resample.
assert.InDelta(t, parsed.Coords[0].Lon, profile.Route[0][0], 1e-6)
assert.InDelta(t, parsed.Coords[0].Lat, profile.Route[0][1], 1e-6)
assert.InDelta(t, parsed.Coords[1].Lon, profile.Route[1][0], 1e-6)
assert.InDelta(t, parsed.Coords[1].Lat, profile.Route[1][1], 1e-6)
}