using System.IO.Compression;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using JobTrackerApi.Controllers;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class AccountDataExportTests
{
    [Fact]
    public async Task Readable_zip_is_complete_checksummed_owner_isolated_and_redacted()
    {
        var root = Path.Combine(Path.GetTempPath(), $"jobtracker-account-export-{Guid.NewGuid():N}");
        Directory.CreateDirectory(root);
        try
        {
            var (db, paths, service) = CreateService(root);
            await using (db)
            {
                var owner = new ApplicationUser
                {
                    Id = "user-1",
                    UserName = "owner@example.test",
                    Email = "owner@example.test",
                    DisplayName = "Synthetic Owner",
                    PasswordHash = "PASSWORD_HASH_MUST_NOT_EXPORT",
                    SecurityStamp = "SECURITY_STAMP_MUST_NOT_EXPORT",
                    TotpSecretEncrypted = "TOTP_SECRET_MUST_NOT_EXPORT",
                    GoogleEmail = "owner@gmail.test",
                    AiEnabled = true,
                };
                var other = new ApplicationUser { Id = "user-2", UserName = "other@example.test", Email = "other@example.test", DisplayName = "OTHER_TENANT_PRIVATE" };
                var role = new IdentityRole("Pro") { Id = "role-pro", NormalizedName = "PRO" };
                db.Users.AddRange(owner, other);
                db.Roles.Add(role);
                db.UserRoles.Add(new IdentityUserRole<string> { UserId = owner.Id, RoleId = role.Id });
                var company = new Company { OwnerUserId = owner.Id, Name = "Owner Company" };
                db.Companies.Add(company);
                await db.SaveChangesAsync();
                var application = new JobApplication { OwnerUserId = owner.Id, CompanyId = company.Id, JobTitle = "Owner Role", Notes = "Readable application note" };
                var otherApplication = new JobApplication { OwnerUserId = other.Id, CompanyId = company.Id, JobTitle = "OTHER_TENANT_PRIVATE" };
                db.JobApplications.AddRange(application, otherApplication);
                await db.SaveChangesAsync();

                var attachmentPath = Path.Combine(paths.AttachmentsRoot, application.Id.ToString(), "evidence.txt");
                Directory.CreateDirectory(Path.GetDirectoryName(attachmentPath)!);
                await File.WriteAllTextAsync(attachmentPath, "owned attachment bytes");
                db.Attachments.Add(new Attachment { JobApplicationId = application.Id, FileName = "evidence.txt", FilePath = attachmentPath, FileType = "text/plain", FileSize = new FileInfo(attachmentPath).Length });

                var artifactRoot = Path.Combine(paths.CvArtifactsRoot, AppPaths.GetOwnerStorageKey(owner.Id));
                Directory.CreateDirectory(artifactRoot);
                var artifactPath = Path.Combine(artifactRoot, "resume.txt");
                await File.WriteAllTextAsync(artifactPath, "owned CV artifact bytes");
                db.CvUploadArtifacts.Add(new CvUploadArtifact { OwnerUserId = owner.Id, OriginalFileName = "resume.txt", StoredFileName = "resume.txt", MimeType = "text/plain", ByteSize = new FileInfo(artifactPath).Length, Sha256 = "synthetic", StoragePath = artifactPath });
                db.GmailConnections.Add(new GmailConnection { OwnerUserId = owner.Id, GmailAddress = owner.GoogleEmail!, Scope = "mail.read", EncryptedRefreshToken = "GMAIL_REFRESH_SECRET_MUST_NOT_EXPORT", EncryptedAccessToken = "GMAIL_ACCESS_SECRET_MUST_NOT_EXPORT" });
                db.ImapConnections.Add(new ImapConnection { OwnerUserId = owner.Id, Host = "mail.example.test", Username = "owner", EncryptedPassword = "IMAP_SECRET_MUST_NOT_EXPORT" });
                db.UserOperations.Add(new UserOperation { Id = Guid.NewGuid(), OwnerUserId = owner.Id, TaskType = "synthetic", Status = OperationStatuses.Running, LeaseToken = "LEASE_SECRET_MUST_NOT_EXPORT", CreatedAtUtc = DateTime.UtcNow, AvailableAtUtc = DateTime.UtcNow });
                db.AiUsageRecords.Add(new AiUsageRecord
                {
                    OwnerUserId = owner.Id, SourceType = "workspace", SourceId = "export-proof",
                    TaskType = "ai.workspace", InputCharacterCount = 120, OutputCharacterCount = 40,
                    EstimatedTokenCount = 40, CreatedAtUtc = DateTimeOffset.UtcNow,
                });
                db.TwoFactorRecoveryCodes.Add(new TwoFactorRecoveryCode { UserId = owner.Id, CodeHash = "RECOVERY_HASH_MUST_NOT_EXPORT", CreatedAtUtc = DateTimeOffset.UtcNow });
                await db.SaveChangesAsync();

                var avatar = await AvatarStorage.StoreAsync(paths.DataRoot, owner.Id, [1, 2, 3, 4], "image/png", CancellationToken.None);
                owner.AvatarImageDataUrl = avatar;
                db.Users.Update(owner);
                var generatedCvRoot = Path.Combine(paths.GetOwnerCvExportsRoot(owner.Id), "20260815");
                Directory.CreateDirectory(generatedCvRoot);
                await File.WriteAllTextAsync(Path.Combine(generatedCvRoot, "generated.pdf"), "synthetic generated PDF");
                var dailyRoot = paths.GetOwnerDailyExportsRoot(null, owner.Id);
                Directory.CreateDirectory(dailyRoot);
                await File.WriteAllTextAsync(Path.Combine(dailyRoot, "daily_export_20260815.json"), "{\"owner\":true}");
                await db.SaveChangesAsync();

                var artifact = await service.CreateAsync(owner.Id, CancellationToken.None);
                Assert.True(File.Exists(artifact.StoragePath));
                using var archive = ZipFile.OpenRead(artifact.StoragePath);
                Assert.NotNull(archive.GetEntry("manifest.json"));
                Assert.NotNull(archive.GetEntry("README.txt"));
                Assert.NotNull(archive.GetEntry("data/account.json"));
                Assert.NotNull(archive.GetEntry("data/applications.json"));
                Assert.NotNull(archive.GetEntry($"files/attachments/{application.Id}/evidence.txt"));
                Assert.NotNull(archive.GetEntry("files/cv-artifacts/1/resume.txt"));
                Assert.Contains(archive.Entries, item => item.FullName.StartsWith("files/avatar/", StringComparison.Ordinal));
                Assert.NotNull(archive.GetEntry("files/generated-cv/20260815/generated.pdf"));
                Assert.NotNull(archive.GetEntry("files/daily-exports/daily_export_20260815.json"));

                var readable = string.Join('\n', archive.Entries
                    .Where(item => item.FullName.EndsWith(".json", StringComparison.OrdinalIgnoreCase) || item.FullName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
                    .Select(ReadText));
                Assert.Contains("Synthetic Owner", readable);
                Assert.Contains("Readable application note", readable);
                Assert.Contains("export-proof", readable);
                Assert.DoesNotContain("OTHER_TENANT_PRIVATE", readable);
                Assert.DoesNotContain("PASSWORD_HASH_MUST_NOT_EXPORT", readable);
                Assert.DoesNotContain("SECURITY_STAMP_MUST_NOT_EXPORT", readable);
                Assert.DoesNotContain("TOTP_SECRET_MUST_NOT_EXPORT", readable);
                Assert.DoesNotContain("GMAIL_REFRESH_SECRET_MUST_NOT_EXPORT", readable);
                Assert.DoesNotContain("GMAIL_ACCESS_SECRET_MUST_NOT_EXPORT", readable);
                Assert.DoesNotContain("IMAP_SECRET_MUST_NOT_EXPORT", readable);
                Assert.DoesNotContain("LEASE_SECRET_MUST_NOT_EXPORT", readable);
                Assert.DoesNotContain("RECOVERY_HASH_MUST_NOT_EXPORT", readable);

                using var manifestDocument = JsonDocument.Parse(ReadText(archive.GetEntry("manifest.json")!));
                var manifestEntries = manifestDocument.RootElement.GetProperty("entries").EnumerateArray().ToList();
                Assert.Equal(archive.Entries.Count - 1, manifestEntries.Count);
                foreach (var manifestEntry in manifestEntries)
                {
                    var zipEntry = archive.GetEntry(manifestEntry.GetProperty("path").GetString()!);
                    Assert.NotNull(zipEntry);
                    using var source = zipEntry!.Open();
                    var hash = Convert.ToHexString(SHA256.HashData(source)).ToLowerInvariant();
                    Assert.Equal(manifestEntry.GetProperty("sha256").GetString(), hash);
                    Assert.Equal(manifestEntry.GetProperty("bytes").GetInt64(), zipEntry.Length);
                }
            }
        }
        finally
        {
            SqliteConnection.ClearAllPools();
            if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
        }
    }

    [Fact]
    public async Task Account_export_requires_a_session_created_within_fifteen_minutes()
    {
        var root = Path.Combine(Path.GetTempPath(), $"jobtracker-account-export-auth-{Guid.NewGuid():N}");
        Directory.CreateDirectory(root);
        try
        {
            var (db, _, service) = CreateService(root);
            await using (db)
            {
                var user = new ApplicationUser { Id = "user-1", UserName = "owner@example.test", Email = "owner@example.test" };
                db.Users.Add(user);
                db.UserSessions.AddRange(
                    new UserSession { Id = "fresh", UserId = user.Id, CreatedAtUtc = DateTimeOffset.UtcNow, LastSeenAtUtc = DateTimeOffset.UtcNow, ExpiresAtUtc = DateTimeOffset.UtcNow.AddHours(1) },
                    new UserSession { Id = "stale", UserId = user.Id, CreatedAtUtc = DateTimeOffset.UtcNow.AddMinutes(-16), LastSeenAtUtc = DateTimeOffset.UtcNow, ExpiresAtUtc = DateTimeOffset.UtcNow.AddHours(1) });
                await db.SaveChangesAsync();

                var stale = Controller(db, service, user.Id, "stale");
                var staleResult = Assert.IsType<ObjectResult>(await stale.ExportAccount(CancellationToken.None));
                Assert.Equal(StatusCodes.Status403Forbidden, staleResult.StatusCode);

                var fresh = Controller(db, service, user.Id, "fresh");
                var file = Assert.IsType<FileStreamResult>(await fresh.ExportAccount(CancellationToken.None));
                Assert.Equal("application/zip", file.ContentType);
                Assert.EndsWith(".zip", file.FileDownloadName);
                await file.FileStream.DisposeAsync();
            }
        }
        finally
        {
            SqliteConnection.ClearAllPools();
            if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
        }
    }

    [Fact]
    public void Account_export_is_rate_limited()
    {
        var attribute = typeof(ExportController).GetMethod(nameof(ExportController.ExportAccount))!.GetCustomAttributes(typeof(EnableRateLimitingAttribute), inherit: true).Cast<EnableRateLimitingAttribute>().Single();
        Assert.Equal("account-data", attribute.PolicyName);
    }

    private static ExportController Controller(JobTrackerApi.Data.JobTrackerContext db, AccountDataExportService service, string userId, string sessionId)
    {
        var controller = new ExportController(db, service) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } };
        controller.ControllerContext.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(
        [
            new Claim(ClaimTypes.NameIdentifier, userId),
            new Claim("sid", sessionId),
        ], "local"));
        return controller;
    }

    private static (JobTrackerApi.Data.JobTrackerContext Db, AppPaths Paths, AccountDataExportService Service) CreateService(string root)
    {
        var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?> { ["Data:Root"] = root }).Build();
        var environment = new Mock<IHostEnvironment>();
        environment.SetupGet(item => item.ContentRootPath).Returns(root);
        var paths = new AppPaths(configuration, environment.Object);
        var currentUser = new Mock<ICurrentUserService>();
        currentUser.SetupGet(item => item.UserId).Returns("user-1");
        var options = new DbContextOptionsBuilder<JobTrackerContext>()
            .UseSqlite($"Data Source={Path.Combine(root, "account-export-tests.db")}")
            .Options;
        var db = new JobTrackerContext(options, currentUser.Object);
        db.Database.EnsureCreated();
        var inventory = new AccountOwnedFileInventory(db, paths, new AttachmentStorage(paths));
        return (db, paths, new AccountDataExportService(db, paths, inventory, TimeProvider.System));
    }

    private static string ReadText(ZipArchiveEntry entry)
    {
        using var reader = new StreamReader(entry.Open(), Encoding.UTF8);
        return reader.ReadToEnd();
    }
}
