using System.Text;
using DeepDrftContent.Constants;
using DeepDrftContent.Processors;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using FileDb = DeepDrftContent.FileDatabase.Services.FileDatabase;

namespace DeepDrftTests;

/// <summary>
/// Integration tests for the per-track high-res waveform compute (phase-12 §5, Direction B). These
/// exercise the exact content-side path the upload, CMS generate action, and Mix trigger all funnel
/// through (<see cref="WaveformProfileService.ComputeAndStoreHighResAsync"/>) over a real
/// <see cref="FileDb"/> and a real <see cref="AudioProcessor"/> + <see cref="RmsLoudnessAlgorithm"/>.
/// The track's medium is irrelevant here — that is the point of the generalization: the content
/// service computes a datum from any track's audio, keyed by EntryKey, with no Mix coupling.
/// </summary>
[TestFixture]
public class WaveformProfileServiceTests
{
    private string _testDir = string.Empty;

    [SetUp]
    public void SetUp()
    {
        _testDir = Path.Combine(Path.GetTempPath(), "WaveformProfileServiceTests", Guid.NewGuid().ToString());
        Directory.CreateDirectory(_testDir);
    }

    [TearDown]
    public void TearDown()
    {
        try { Directory.Delete(_testDir, recursive: true); }
        catch { /* Best-effort cleanup — ignore failures */ }
    }

    private async Task<WaveformProfileService> CreateServiceAsync(FileDb fileDatabase)
    {
        await Task.CompletedTask;
        return new WaveformProfileService(
            fileDatabase,
            new AudioProcessor(),
            new RmsLoudnessAlgorithm(),
            Options.Create(new WaveformProfileOptions()),
            NullLogger<WaveformProfileService>.Instance);
    }

    [Test]
    public async Task ComputeAndStoreHighResAsync_NonMixTrack_StoresDatumInTrackWaveformsVault()
    {
        var fileDatabase = await FileDb.FromAsync(_testDir);
        Assert.That(fileDatabase, Is.Not.Null);
        var service = await CreateServiceAsync(fileDatabase!);

        // A 2-second mono 16-bit WAV — stands in for "any track" (Cut/Session/Mix alike). No release
        // or medium is involved; the compute is keyed only by the supplied EntryKey.
        const string entryKey = "cut-track-entry";
        var wav = BuildMinimalPcmWav(durationSeconds: 2.0);

        var stored = await service.ComputeAndStoreHighResAsync(wav, entryKey, durationSeconds: 2.0);

        Assert.That(stored, Is.True, "High-res compute should succeed for a decodable PCM WAV");

        var datum = await service.GetProfileAsync(entryKey, VaultConstants.TrackWaveforms);
        Assert.That(datum, Is.Not.Null, "Datum must be retrievable from the track-waveforms vault by EntryKey");
        // 2 s × 333/s = 666 buckets, below the floor → clamps to the floor (2048).
        Assert.That(datum!.Length, Is.EqualTo(WaveformResolution.BucketCountForDuration(2.0)));
        Assert.That(datum.Length, Is.EqualTo(WaveformResolution.MinBucketCount));
    }

    [Test]
    public async Task ComputeAndStoreHighResAsync_LongTrack_BucketCountIsDurationDerived()
    {
        var fileDatabase = await FileDb.FromAsync(_testDir);
        var service = await CreateServiceAsync(fileDatabase!);

        // A 10-second WAV: 10 × 333 = 3330 buckets, above the 2048 floor — proves the count tracks
        // duration rather than the fixed 512-bucket profile resolution.
        const string entryKey = "long-track-entry";
        var wav = BuildMinimalPcmWav(durationSeconds: 10.0);

        var stored = await service.ComputeAndStoreHighResAsync(wav, entryKey, durationSeconds: 10.0);
        Assert.That(stored, Is.True);

        var datum = await service.GetProfileAsync(entryKey, VaultConstants.TrackWaveforms);
        Assert.That(datum, Is.Not.Null);
        Assert.That(datum!.Length, Is.EqualTo(WaveformResolution.BucketCountForDuration(10.0)));
        Assert.That(datum.Length, Is.GreaterThan(new WaveformProfileOptions().BucketCount),
            "The high-res datum must be denser than the fixed 512-bucket player-bar profile");
    }

    [Test]
    public async Task HighResAndProfile_ForSameTrack_StoredInSeparateVaultsKeyedByEntryKey()
    {
        var fileDatabase = await FileDb.FromAsync(_testDir);
        var service = await CreateServiceAsync(fileDatabase!);

        // The two datums a track carries (phase-12 §5): the 512-bucket player-bar profile and the
        // duration-derived high-res visualizer datum. Both key off the same EntryKey but live in
        // distinct vaults, so neither overwrites the other.
        const string entryKey = "shared-key";
        var wav = BuildMinimalPcmWav(durationSeconds: 10.0);

        Assert.That(await service.ComputeAndStoreAsync(wav, entryKey), Is.True);
        Assert.That(await service.ComputeAndStoreHighResAsync(wav, entryKey, durationSeconds: 10.0), Is.True);

        var profile = await service.GetProfileAsync(entryKey);
        var highRes = await service.GetProfileAsync(entryKey, VaultConstants.TrackWaveforms);

        Assert.Multiple(() =>
        {
            Assert.That(profile, Is.Not.Null);
            Assert.That(highRes, Is.Not.Null);
            Assert.That(profile!.Length, Is.EqualTo(new WaveformProfileOptions().BucketCount));
            Assert.That(highRes!.Length, Is.EqualTo(WaveformResolution.BucketCountForDuration(10.0)));
            Assert.That(highRes.Length, Is.Not.EqualTo(profile.Length), "The two datums must differ in resolution");
        });
    }

    [Test]
    public async Task ComputeAndStoreHighResAsync_IsRerunnable_OverwritesPriorDatum()
    {
        var fileDatabase = await FileDb.FromAsync(_testDir);
        var service = await CreateServiceAsync(fileDatabase!);

        // The backfill / regenerate path must be re-runnable: a second compute for the same key
        // overwrites cleanly rather than failing or duplicating.
        const string entryKey = "rerun-key";
        var wav = BuildMinimalPcmWav(durationSeconds: 10.0);

        Assert.That(await service.ComputeAndStoreHighResAsync(wav, entryKey, durationSeconds: 10.0), Is.True);
        Assert.That(await service.ComputeAndStoreHighResAsync(wav, entryKey, durationSeconds: 10.0), Is.True,
            "A re-run must succeed and overwrite the prior datum");

        var datum = await service.GetProfileAsync(entryKey, VaultConstants.TrackWaveforms);
        Assert.That(datum, Is.Not.Null);
        Assert.That(datum!.Length, Is.EqualTo(WaveformResolution.BucketCountForDuration(10.0)));
    }

    // Builds a minimal standard-PCM mono 16-bit 44.1 kHz WAV with a full-scale square wave across the
    // requested duration. Real PCM (not silence) so the loudness algorithm produces a non-degenerate
    // envelope. Mirrors the chunk layout AudioProcessor expects (RIFF/WAVE/fmt /data).
    private static byte[] BuildMinimalPcmWav(double durationSeconds)
    {
        const int sampleRate = 44100;
        const ushort channels = 1;
        const ushort bitsPerSample = 16;
        const ushort blockAlign = channels * (bitsPerSample / 8);
        const uint byteRate = sampleRate * blockAlign;

        var frames = (int)(sampleRate * durationSeconds);
        var data = new byte[frames * blockAlign];
        for (var i = 0; i < frames; i++)
        {
            // Alternating full-scale square wave so RMS reads as loud, not silent.
            var sample = (i % 2 == 0) ? short.MaxValue : short.MinValue;
            data[i * 2] = (byte)(sample & 0xFF);
            data[i * 2 + 1] = (byte)((sample >> 8) & 0xFF);
        }

        using var ms = new MemoryStream();
        using var w = new BinaryWriter(ms, Encoding.ASCII, leaveOpen: true);

        w.Write(Encoding.ASCII.GetBytes("RIFF"));
        w.Write((uint)(36 + data.Length));
        w.Write(Encoding.ASCII.GetBytes("WAVE"));

        w.Write(Encoding.ASCII.GetBytes("fmt "));
        w.Write(16u);
        w.Write((ushort)1); // PCM
        w.Write(channels);
        w.Write((uint)sampleRate);
        w.Write(byteRate);
        w.Write(blockAlign);
        w.Write(bitsPerSample);

        w.Write(Encoding.ASCII.GetBytes("data"));
        w.Write((uint)data.Length);
        w.Write(data);

        w.Flush();
        return ms.ToArray();
    }
}
