using System.Text;
using System.Text.Json;
using Kit.Helpers.SqlHelperAbstract;
using Server.WatchBody.Core.Dal.Src.Domain;
using Server.WatchBody.Core.Dal.Src.Repository;
using Server.WatchBody.Fetch;
using Server.WatchBody.Import;
using Xunit;
namespace Server.WatchBody.Web.Tests;
///
/// Интеграционные тесты m07 (specs 01–03) против docker watchbody-pg
/// с фейковым Garmin (FakeGarminHandler); .fit — энкодер SDK.
///
[Collection("web")]
public class FitFetcherTests(TestApp app)
{
private static readonly DateTime Start = new(2026, 10, 15, 18, 0, 0, DateTimeKind.Utc);
private ConnectionString NewCs() => new()
{
Type = ConnectionStringType.Postgres,
Value = app.Config.DbConnectionString,
};
private int NewAthlete(string name)
{
var cs = NewCs();
var athleteId = new AthleteRepository(cs).Insert(new Athlete
{
Name = name,
ApiKeyHash = Server.WatchBody.Web.Services.ApiKeyHash.Of(Guid.NewGuid().ToString()),
});
return athleteId;
}
private static (string Dir, string Base) NewEnv()
{
var dir = Path.Combine(Path.GetTempPath(), "wb-fetch-" + Guid.NewGuid().ToString("N")[..8]);
Directory.CreateDirectory(dir);
return (dir, "http://fake-garmin.test");
}
private static byte[] StrengthFit() =>
FitBytes(Start, "123456-20261015T180000Z");
private static byte[] FitBytes(DateTime start, string _) // serial задаётся энкодером
{
var path = Path.Combine(Path.GetTempPath(), "wb-fit-" + Guid.NewGuid().ToString("N")[..8] + ".fit");
FitParser.WriteTestFile(path, start,
records: [(0, 100), (60, 110)], laps: [(0, 0, 300)], sets: [(10, 80.0m)], serial: "777");
var bytes = File.ReadAllBytes(path);
File.Delete(path);
return bytes;
}
private FitFetcher NewFetcher(string dir, FakeGarminHandler handler, string athleteName) =>
new(NewCs(), new GarminClient("owner@example.com", "secret", "http://fake-garmin.test", handler),
dir, athleteName);
// --- spec 02: цикл ---
[Fact]
public void Fetch_StrengthOnly_DownloadedAndImported()
{
var athlete = NewAthlete("f7-" + Guid.NewGuid().ToString("N")[..8]);
var (dir, _) = NewEnv();
var handler = new FakeGarminHandler
{
Activities =
[
new GarminActivity(1001, Start, "strength_training"),
new GarminActivity(1002, Start.AddHours(-2), "cardio"), // не силовая
],
FitFiles = { [1001] = StrengthFit() },
};
var (downloaded, skipped, failed) = NewFetcher(dir, handler, athleteName(athlete)).FetchNew();
Assert.Equal((1, 0, 0), (downloaded, skipped, failed));
Assert.Equal(1, handler.DownloadCalls); // cardio не скачивается
var row = new WorkoutRepository(NewCs()).Select(new WorkoutFilter
{
AthleteId = athlete,
DateFrom = new DateOnly(2026, 10, 14),
DateTo = new DateOnly(2026, 10, 16),
}).Single(w => w.FitFileId != null);
Assert.NotNull(row.FactsJson);
Assert.Equal(105, row.AvgHr);
Assert.True(File.Exists(Path.Combine(dir, "1001.fit"))); // кэш
// повторный fetch — всё в кэше, скачиваний нет, дублей нет
var second = NewFetcher(dir, handler, athleteName(athlete)).FetchNew();
Assert.Equal((0, 1, 0), second);
Assert.Equal(1, handler.DownloadCalls);
}
[Fact]
public void Fetch_DuplicateFitFileId_FileDeletedNoSecondRow()
{
var athlete = NewAthlete("f7-dup-" + Guid.NewGuid().ToString("N")[..8]);
var (dir, _) = NewEnv();
var handler = new FakeGarminHandler
{
Activities = { new GarminActivity(2001, Start, "strength_training") },
FitFiles = { [2001] = StrengthFit() },
};
Assert.Equal((1, 0, 0), NewFetcher(dir, handler, athleteName(athlete)).FetchNew());
// тот же .fit под другим activityId (переименование в Garmin)
handler.Activities.Add(new GarminActivity(2002, Start, "strength_training"));
handler.FitFiles[2002] = StrengthFit();
var (downloaded, skipped, failed) = NewFetcher(dir, handler, athleteName(athlete)).FetchNew();
// 2001 — кэш (рубеж 1), 2002 — дубль fit_file_id (рубеж 2)
Assert.Equal((0, 2, 0), (downloaded, skipped, failed));
Assert.False(File.Exists(Path.Combine(dir, "2002.fit"))); // дубль не кэшируется
var rows = new WorkoutRepository(NewCs()).Select(new WorkoutFilter
{
AthleteId = athlete,
DateFrom = new DateOnly(2026, 10, 14),
DateTo = new DateOnly(2026, 10, 16),
}).Count(w => w.FitFileId != null);
Assert.Equal(1, rows); // второй строки нет
}
[Fact]
public void Fetch_BrokenDownload_MarkedBad_Continues()
{
var athlete = NewAthlete("f7-bad-" + Guid.NewGuid().ToString("N")[..8]);
var (dir, _) = NewEnv();
var handler = new FakeGarminHandler
{
Activities =
[
new GarminActivity(3001, Start, "strength_training"),
new GarminActivity(3002, Start.AddHours(-1), "strength_training"),
],
FitFiles = { [3001] = [1, 2, 3], [3002] = StrengthFit() },
};
var (downloaded, skipped, failed) = NewFetcher(dir, handler, athleteName(athlete)).FetchNew();
Assert.Equal((1, 0, 1), (downloaded, skipped, failed));
Assert.True(File.Exists(Path.Combine(dir, "3001.fit.bad")));
Assert.True(File.Exists(Path.Combine(dir, "3002.fit")));
}
[Fact]
public void Fetch_UnknownAthlete_Throws()
{
var (dir, _) = NewEnv();
var handler = new FakeGarminHandler
{
Activities = { new GarminActivity(4001, Start, "strength_training") },
FitFiles = { [4001] = StrengthFit() },
};
var fetcher = NewFetcher(dir, handler, "нет-такого-атлета");
Assert.Throws(() => fetcher.FetchNew());
Assert.Equal(0, handler.LoginCalls); // атлет проверяется до сети
}
[Fact]
public void Fetch_AuthFailure_ThrowsWithoutPassword()
{
var athlete = NewAthlete("f7-auth-" + Guid.NewGuid().ToString("N")[..8]);
var (dir, _) = NewEnv();
var handler = new FakeGarminHandler { FailLogin = true };
var fetcher = NewFetcher(dir, handler, athleteName(athlete));
var exception = Assert.Throws(() => fetcher.FetchNew());
Assert.DoesNotContain("secret", exception.Message); // 🛡4: пароль не в тексте
Assert.Equal(1, handler.LoginCalls);
}
private string athleteName(int athleteId) =>
new AthleteRepository(NewCs()).Select(new AthleteFilter())
.First(a => a.Id == athleteId).Name;
}