using FluentAssertions;
using foodmarket.Api.Background;
using foodmarket.Domain.Organizations;
using foodmarket.Infrastructure.Persistence;
using foodmarket.IntegrationTests.Support;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace foodmarket.IntegrationTests;
/// Sprint 28: тест для
/// (введён в Sprint 25). Требует реальный PostgreSQL — метод использует
/// information_schema + DO $$ блоки, которых нет в SQLite.
///
/// Проверяет:
/// - Только org'и с именем quality-% старше N часов удаляются.
/// - Свежие quality-* не трогаются.
/// - Не-quality org'и (включая Test-*, реальные имена) не трогаются.
/// - FK-loop ретрая работает (нет foreign_key_violation при множественных
/// зависимостях employees ↔ employee_roles).
///
[Collection(ApiCollection.Name)]
public class PruneQualityTestOrgsTests
{
private readonly ApiFactory _factory;
public PruneQualityTestOrgsTests(ApiFactory factory) => _factory = factory;
private static IConfiguration CfgWith(int hours) =>
new ConfigurationBuilder().AddInMemoryCollection(new Dictionary
{
["Cleanup:QualityTestOrgHours"] = hours.ToString(),
}).Build();
[Fact]
public async Task Deletes_only_quality_orgs_older_than_threshold()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
// Сетап: 4 org'и — quality-old (старая), quality-new (свежая),
// real-old (реальное имя, не трогаем), quality-edge (на грани).
var oldQuality = new Organization
{
Id = Guid.NewGuid(),
Name = "quality-old-test-1",
CreatedAt = DateTime.UtcNow.AddHours(-48),
};
var newQuality = new Organization
{
Id = Guid.NewGuid(),
Name = "quality-new-test-2",
CreatedAt = DateTime.UtcNow.AddMinutes(-30),
};
var realOld = new Organization
{
Id = Guid.NewGuid(),
Name = "Real Shop LLC",
CreatedAt = DateTime.UtcNow.AddDays(-30),
};
var edge = new Organization
{
Id = Guid.NewGuid(),
Name = "quality-edge-test-3",
// На самой границе threshold (24h) с небольшим запасом.
CreatedAt = DateTime.UtcNow.AddHours(-25),
};
db.Organizations.AddRange(oldQuality, newQuality, realOld, edge);
await db.SaveChangesAsync();
// Threshold = 24 часа.
var jobs = new HousekeepingJobs(db, CfgWith(hours: 24), NullLogger.Instance);
var deleted = await jobs.PruneQualityTestOrgsAsync();
// Удалены должны быть oldQuality + edge (24h+ старые с quality-* префиксом).
deleted.Should().Be(2);
using var scope2 = _factory.Services.CreateScope();
var db2 = scope2.ServiceProvider.GetRequiredService();
var remaining = await db2.Organizations
.IgnoreQueryFilters()
.Where(o => new[] { oldQuality.Id, newQuality.Id, realOld.Id, edge.Id }.Contains(o.Id))
.Select(o => o.Name)
.ToListAsync();
remaining.Should().Contain("quality-new-test-2", "свежий quality-* не удаляется");
remaining.Should().Contain("Real Shop LLC", "реальная org не удаляется");
remaining.Should().NotContain("quality-old-test-1", "старая quality-* удалена");
remaining.Should().NotContain("quality-edge-test-3", "edge-старше-threshold удалена");
}
[Fact]
public async Task Returns_zero_when_no_candidates_match()
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
// Только свежая quality-* — она НЕ должна удаляться.
var fresh = new Organization
{
Id = Guid.NewGuid(),
Name = $"quality-fresh-{Guid.NewGuid():N}",
CreatedAt = DateTime.UtcNow,
};
db.Organizations.Add(fresh);
await db.SaveChangesAsync();
var jobs = new HousekeepingJobs(db, CfgWith(hours: 24), NullLogger.Instance);
var deleted = await jobs.PruneQualityTestOrgsAsync();
deleted.Should().Be(0);
using var scope2 = _factory.Services.CreateScope();
var db2 = scope2.ServiceProvider.GetRequiredService();
var stillExists = await db2.Organizations
.IgnoreQueryFilters()
.AnyAsync(o => o.Id == fresh.Id);
stillExists.Should().BeTrue();
}
}