using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Jobs;
using Tiku.Application.Tenancy;
using Tiku.Domain.Identity;
using Tiku.Domain.Operations;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.IntegrationTests.Api;

public sealed class P0OperationsLifecycleTests
{
    [Fact]
    public async Task Job_idempotency_cancel_retry_and_tenant_scope_follow_the_reviewed_state_machine()
    {
        await using var factory = new ApiTestFactory();
        var tenantA = await SeedTenantAsync(factory);
        var tenantB = await SeedTenantAsync(factory);
        using var scope = factory.CreateSystemScope("Verify P0 job state machine");
        var jobs = scope.ServiceProvider.GetRequiredService<IBackgroundJobService>();
        var command = new CreateBackgroundJobCommand(
            tenantA.TenantId,
            "asset_security_scan",
            JsonSerializer.SerializeToElement(new { assetId = Guid.NewGuid() }),
            IdempotencyKey: "same-request",
            IsSystemJob: true);

        var first = await jobs.EnqueueAsync(command);
        var duplicate = await jobs.EnqueueAsync(command);
        Assert.Equal(first.Id, duplicate.Id);
        Assert.Null(await jobs.GetAsync(first.Id, tenantB.TenantId));

        var cancelled = await jobs.RequestCancellationAsync(
            first.Id, tenantA.TenantId, tenantA.UserId, "No longer required");
        Assert.Equal(BackgroundJobStatus.Cancelled, cancelled.Status);
        Assert.NotNull(cancelled.CancellationRequestedAt);
        var retried = await jobs.RetryAsync(first.Id, tenantA.TenantId, tenantA.UserId);
        Assert.Equal(BackgroundJobStatus.Pending, retried.Status);
        Assert.Null(retried.CancellationRequestedAt);
        await Assert.ThrowsAsync<BackgroundJobException>(() =>
            jobs.RetryAsync(first.Id, tenantA.TenantId, tenantA.UserId));

        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        await dbContext.BackgroundJobs.Where(item => item.Id == first.Id)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(item => item.Status, BackgroundJobStatus.Processing)
                .SetProperty(item => item.LockedBy, "active-worker")
                .SetProperty(item => item.LockExpiresAt, DateTimeOffset.UtcNow.AddMinutes(5)));
        var cooperative = await jobs.RequestCancellationAsync(
            first.Id, tenantA.TenantId, tenantA.UserId, "Stop at the next cooperative boundary");
        Assert.Equal(BackgroundJobStatus.Processing, cooperative.Status);
        Assert.NotNull(cooperative.CancellationRequestedAt);
    }

    [Fact]
    public async Task Tenant_export_archive_restore_and_owner_transfer_preserve_lifecycle_invariants()
    {
        await using var factory = new ApiTestFactory(
            configurationOverrides: new Dictionary<string, string?>
            {
                ["Storage:DefaultProvider"] = "local_dev",
                ["Storage:DefaultBucket"] = "tenant-assets"
            });
        var seed = await SeedTenantAsync(factory, true);
        Guid exportOperationId;
        using (var scope = factory.CreateSystemScope("Create tenant export operation"))
        {
            var lifecycle = scope.ServiceProvider.GetRequiredService<ITenantLifecycleService>();
            var blocked = await lifecycle.PreviewArchiveAsync(seed.TenantId);
            Assert.False(blocked.CanArchive);
            Assert.Contains("recent_successful_export_required", blocked.Blockers);
            exportOperationId = (await lifecycle.CreateExportAsync(seed.TenantId, seed.UserId)).Id;
        }

        using (var scope = factory.CreateSystemScope("Process tenant export operation"))
        {
            var processed = await scope.ServiceProvider.GetRequiredService<IBackgroundJobService>()
                .ProcessPendingAsync("tenant-export-test", 10);
            Assert.Equal(1, processed);
        }

        using (var scope = factory.CreateSystemScope("Archive restore and transfer tenant"))
        {
            var lifecycle = scope.ServiceProvider.GetRequiredService<ITenantLifecycleService>();
            var export = await lifecycle.GetOperationAsync(seed.TenantId, exportOperationId);
            var exportJob = await scope.ServiceProvider.GetRequiredService<TikuDbContext>().BackgroundJobs
                .AsNoTracking()
                .SingleAsync(item => item.TenantId == seed.TenantId && item.JobType == "tenant_export");
            Assert.True(
                export!.Status == TenantLifecycleOperationStatus.Succeeded,
                $"Export status={export.Status}, operationError={export.LastError}, jobStatus={exportJob.Status}, jobError={exportJob.LastError}");
            Assert.NotNull(export.ExportAssetId);
            Assert.True((await lifecycle.PreviewArchiveAsync(seed.TenantId)).CanArchive);
            await lifecycle.ArchiveAsync(seed.TenantId, seed.UserId, "Contract ended");

            var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
            Assert.Equal(TenantStatus.Archived,
                await dbContext.Tenants.Where(item => item.Id == seed.TenantId).Select(item => item.Status)
                    .SingleAsync());
            Assert.All(
                await dbContext.TenantDomains.Where(item => item.TenantId == seed.TenantId).Select(item => item.Status)
                    .ToArrayAsync(),
                status => Assert.Equal(TenantDomainStatus.Disabled, status));

            await lifecycle.RestoreAsync(seed.TenantId, seed.UserId, "Customer returned");
            Assert.Equal(TenantStatus.Suspended,
                await dbContext.Tenants.Where(item => item.Id == seed.TenantId).Select(item => item.Status)
                    .SingleAsync());
            Assert.All(
                await dbContext.TenantDomains.Where(item => item.TenantId == seed.TenantId).Select(item => item.Status)
                    .ToArrayAsync(),
                status => Assert.Equal(TenantDomainStatus.Pending, status));

            await lifecycle.TransferOwnerAsync(seed.TenantId, seed.UserId, seed.SecondUserId!.Value,
                "Ownership handover");
            Assert.Equal(seed.SecondUserId,
                await dbContext.Tenants.Where(item => item.Id == seed.TenantId).Select(item => item.OwnerUserId)
                    .SingleAsync());
            Assert.Equal(TenantRole.TenantAdmin,
                await dbContext.TenantMemberships
                    .Where(item => item.TenantId == seed.TenantId && item.UserId == seed.UserId)
                    .Select(item => item.Role).SingleAsync());
            Assert.Equal(TenantRole.TenantOwner,
                await dbContext.TenantMemberships
                    .Where(item => item.TenantId == seed.TenantId && item.UserId == seed.SecondUserId)
                    .Select(item => item.Role).SingleAsync());
        }
    }

    private static async Task<TenantSeed> SeedTenantAsync(ApiTestFactory factory, bool includeSecondMember = false)
    {
        var tenantId = Guid.NewGuid();
        var userId = Guid.NewGuid();
        var secondUserId = includeSecondMember ? Guid.NewGuid() : (Guid?)null;
        var entities = new List<object>
        {
            new Tenant
            {
                Id = tenantId, Slug = tenantId.ToString("N"), Name = "Lifecycle tenant", OwnerUserId = userId
            },
            new User { Id = userId, Phone = $"13{Random.Shared.NextInt64(100_000_000, 1_000_000_000)}", Name = "Owner" }
                .WithTestPassword(),
            new TenantMembership
            {
                TenantId = tenantId, UserId = userId, Role = TenantRole.TenantOwner, Status = MembershipStatus.Active
            },
            new TenantDomain
            {
                TenantId = tenantId, Host = $"{tenantId:N}.example.test", Status = TenantDomainStatus.Active,
                IsPrimary = true
            }
        };
        if (secondUserId.HasValue)
        {
            entities.Add(new User
            {
                Id = secondUserId.Value, Phone = $"13{Random.Shared.NextInt64(100_000_000, 1_000_000_000)}",
                Name = "Next owner"
            }.WithTestPassword());
            entities.Add(new TenantMembership
            {
                TenantId = tenantId, UserId = secondUserId.Value, Role = TenantRole.TenantAdmin,
                Status = MembershipStatus.Active
            });
        }

        await factory.SeedAsync(entities.ToArray());
        return new TenantSeed(tenantId, userId, secondUserId);
    }

    private sealed record TenantSeed(Guid TenantId, Guid UserId, Guid? SecondUserId);
}