using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Unity.Cinemachine;
using UnityEditor;
using UnityEngine;
using UnityEngine.Timeline;

namespace Streamingle.Editor.Tests
{
    public sealed class AICameraCutCorrectionRecorderTests
    {
        private string _temporaryRoot;
        private readonly List<TimelineAsset> _timelines =
            new List<TimelineAsset>();

        [SetUp]
        public void SetUp()
        {
            _temporaryRoot = Path.Combine(
                Path.GetTempPath(),
                $"cw_ai_cut_corrections_{Guid.NewGuid():N}");
            Directory.CreateDirectory(_temporaryRoot);
        }

        [TearDown]
        public void TearDown()
        {
            foreach (var timeline in _timelines.Where(value => value != null))
            {
                UnityEngine.Object.DestroyImmediate(timeline);
            }

            _timelines.Clear();
            if (!string.IsNullOrWhiteSpace(_temporaryRoot) &&
                Directory.Exists(_temporaryRoot))
            {
                Directory.Delete(_temporaryRoot, true);
            }
        }

        [Test]
        public void CaptureRecordsKeepMoveAddAndDeleteWithoutRawEditorId()
        {
            var generated = CreateGeneration(
                60,
                720,
                new[] { 0, 60, 180, 600, 720 });
            var timeline = CreateTimeline(
                new[] { 0, 60, 210, 300, 720 });
            const string rawEditorId = "operator-secret@example.com";

            var path = AICameraCutCorrectionRecorder
                .CaptureTimelineForTests(
                    timeline,
                    generated,
                    rawEditorId,
                    Path.Combine(_temporaryRoot, "logs"),
                    new DateTime(
                        2026,
                        8,
                        2,
                        5,
                        4,
                        3,
                        210,
                        DateTimeKind.Utc));

            var json = File.ReadAllText(path);
            Assert.That(
                json,
                Does.Contain(
                    $"\"schemaVersion\": " +
                    $"\"{AICameraCutCorrectionRecorder.CurrentSchemaVersion}\""));
            Assert.That(json, Does.Contain("\"sampleRate\": 60"));
            Assert.That(json, Does.Contain("\"songId\": \"song-1\""));
            Assert.That(json, Does.Contain("\"datasetId\": \"dataset-1\""));
            Assert.That(json, Does.Contain("\"keepCount\": 1"));
            Assert.That(json, Does.Contain("\"moveCount\": 1"));
            Assert.That(json, Does.Contain("\"addCount\": 1"));
            Assert.That(json, Does.Contain("\"deleteCount\": 1"));
            Assert.That(json, Does.Contain("\"deltaFrames\": 30"));
            Assert.That(
                json,
                Does.Contain("editor-style-sha256:"));
            Assert.That(json, Does.Not.Contain(rawEditorId));

            var bytes = File.ReadAllBytes(path);
            Assert.That(
                bytes.Take(3).ToArray(),
                Is.Not.EqualTo(new byte[] { 0xEF, 0xBB, 0xBF }));
            Assert.That(
                Directory.GetFiles(
                    Path.GetDirectoryName(path),
                    "*.tmp-*"),
                Is.Empty);
        }

        [Test]
        public void UnchangedBoundariesAreRecordedAsAcceptedKeeps()
        {
            var generated = CreateGeneration(
                60,
                720,
                new[] { 0, 60, 180, 600, 720 });
            var timeline = CreateTimeline(
                new[] { 0, 60, 180, 600, 720 });

            var path = AICameraCutCorrectionRecorder
                .CaptureTimelineForTests(
                    timeline,
                    generated,
                    "stable-editor",
                    Path.Combine(_temporaryRoot, "logs"),
                    DateTime.UnixEpoch);

            var json = File.ReadAllText(path);
            Assert.That(json, Does.Contain("\"keepCount\": 3"));
            Assert.That(json, Does.Contain("\"moveCount\": 0"));
            Assert.That(json, Does.Contain("\"addCount\": 0"));
            Assert.That(json, Does.Contain("\"deleteCount\": 0"));
        }

        [Test]
        public void SameInputsProduceDeterministicSnapshotContent()
        {
            var generated = CreateGeneration(
                60,
                720,
                new[] { 0, 60, 180, 600, 720 });
            var timeline = CreateTimeline(
                new[] { 0, 60, 210, 300, 720 });
            var capturedUtc = new DateTime(
                2026,
                8,
                2,
                1,
                2,
                3,
                DateTimeKind.Utc);

            var first = AICameraCutCorrectionRecorder
                .CaptureTimelineForTests(
                    timeline,
                    generated,
                    "editor-a",
                    Path.Combine(_temporaryRoot, "logs-a"),
                    capturedUtc);
            var second = AICameraCutCorrectionRecorder
                .CaptureTimelineForTests(
                    timeline,
                    generated,
                    "editor-a",
                    Path.Combine(_temporaryRoot, "logs-b"),
                    capturedUtc);

            Assert.That(
                File.ReadAllText(second),
                Is.EqualTo(File.ReadAllText(first)));
        }

        [Test]
        public void CaptureDoesNotModifyTimelineOrDirtyState()
        {
            var generated = CreateGeneration(
                60,
                720,
                new[] { 0, 60, 180, 600, 720 });
            var timeline = CreateTimeline(
                new[] { 0, 60, 210, 300, 720 });
            var before = timeline.GetOutputTracks()
                .OfType<CinemachineTrack>()
                .Single()
                .GetClips()
                .OrderBy(clip => clip.start)
                .Select(clip => new[]
                {
                    clip.start,
                    clip.duration,
                    clip.easeInDuration,
                    clip.easeOutDuration
                })
                .ToArray();
            var wasDirty = EditorUtility.IsDirty(timeline);

            AICameraCutCorrectionRecorder.CaptureTimelineForTests(
                timeline,
                generated,
                "editor-a",
                Path.Combine(_temporaryRoot, "logs"),
                DateTime.UtcNow);

            var after = timeline.GetOutputTracks()
                .OfType<CinemachineTrack>()
                .Single()
                .GetClips()
                .OrderBy(clip => clip.start)
                .Select(clip => new[]
                {
                    clip.start,
                    clip.duration,
                    clip.easeInDuration,
                    clip.easeOutDuration
                })
                .ToArray();
            Assert.That(after, Is.EqualTo(before));
            Assert.That(EditorUtility.IsDirty(timeline), Is.EqualTo(wasDirty));
        }

        [Test]
        public void RejectsMetadataThatIsNotSixtyFps()
        {
            var generated = CreateGeneration(
                30,
                720,
                new[] { 0, 720 });
            var timeline = CreateTimeline(new[] { 0, 720 });

            var exception = Assert.Throws<InvalidDataException>(() =>
                AICameraCutCorrectionRecorder.CaptureTimelineForTests(
                    timeline,
                    generated,
                    "editor-a",
                    Path.Combine(_temporaryRoot, "logs"),
                    DateTime.UtcNow));

            Assert.That(exception.Message, Does.Contain("60 FPS"));
        }

        [Test]
        public void RejectsStalePreviewMetadataHash()
        {
            var generated = CreateGeneration(
                60,
                720,
                new[] { 0, 720 });
            var timeline = CreateTimeline(new[] { 0, 720 });

            var exception = Assert.Throws<InvalidDataException>(() =>
                AICameraCutCorrectionRecorder.CaptureTimelineForTests(
                    timeline,
                    generated,
                    "editor-a",
                    Path.Combine(_temporaryRoot, "logs"),
                    DateTime.UtcNow,
                    new string('0', 64)));

            Assert.That(exception.Message, Does.Contain("different generation"));
        }

        [Test]
        public void RejectsGapBetweenEditedCinemachineClips()
        {
            var generated = CreateGeneration(
                60,
                720,
                new[] { 0, 60, 720 });
            var timeline = CreateTimelineWithGap();

            var exception = Assert.Throws<InvalidDataException>(() =>
                AICameraCutCorrectionRecorder.CaptureTimelineForTests(
                    timeline,
                    generated,
                    "editor-a",
                    Path.Combine(_temporaryRoot, "logs"),
                    DateTime.UtcNow));

            Assert.That(exception.Message, Does.Contain("gap or overlap"));
        }

        [Test]
        public void RejectsSubframeEditedBoundary()
        {
            var generated = CreateGeneration(
                60,
                720,
                new[] { 0, 60, 720 });
            var timeline = CreateTimeline(
                new[] { 0d, 60.4d, 720d });

            var exception = Assert.Throws<InvalidDataException>(() =>
                AICameraCutCorrectionRecorder.CaptureTimelineForTests(
                    timeline,
                    generated,
                    "editor-a",
                    Path.Combine(_temporaryRoot, "logs"),
                    DateTime.UtcNow));

            Assert.That(exception.Message, Does.Contain("not aligned"));
        }

        [Test]
        public void AcceptsImportersMicroscopicHardCutGuard()
        {
            var generated = CreateGeneration(
                60,
                720,
                new[] { 0, 60, 180, 720 });
            var timeline = CreateTimeline(
                new[] { 0, 60, 180, 720 });
            var clips = timeline.GetOutputTracks()
                .OfType<CinemachineTrack>()
                .Single()
                .GetClips()
                .OrderBy(clip => clip.start)
                .ToArray();
            clips[0].duration -= 0.000001d;
            clips[1].duration -= 0.000001d;

            var path = AICameraCutCorrectionRecorder
                .CaptureTimelineForTests(
                    timeline,
                    generated,
                    "editor-a",
                    Path.Combine(_temporaryRoot, "logs"),
                    DateTime.UtcNow);

            Assert.That(File.Exists(path), Is.True);
            Assert.That(
                File.ReadAllText(path),
                Does.Contain("\"keepCount\": 2"));
        }

        private string CreateGeneration(
            int sampleRate,
            int frameCount,
            IReadOnlyList<int> edges)
        {
            var generated = Path.Combine(
                _temporaryRoot,
                $"generated-{Guid.NewGuid():N}");
            Directory.CreateDirectory(generated);
            var hashA = new string('a', 64);
            var hashB = new string('b', 64);
            var hashC = new string('c', 64);
            var metadata =
                "{\n" +
                "  \"songName\": \"Test Song\",\n" +
                "  \"songId\": \"song-1\",\n" +
                "  \"datasetId\": \"dataset-1\",\n" +
                $"  \"sampleRate\": {sampleRate},\n" +
                $"  \"frameCount\": {frameCount},\n" +
                "  \"shotsFile\": \"shots.json\",\n" +
                "  \"outputSha256\": {\n" +
                $"    \"worldCamera\": \"{hashA}\",\n" +
                $"    \"time\": \"{hashB}\",\n" +
                $"    \"shots\": \"{hashC}\"\n" +
                "  }\n" +
                "}\n";
            File.WriteAllText(
                Path.Combine(generated, "metadata.json"),
                metadata,
                new UTF8Encoding(false));

            var shots = new StringBuilder();
            shots.Append("{\n  \"shots\": [\n");
            for (var index = 0; index < edges.Count - 1; index++)
            {
                shots.Append(
                    $"    {{ \"startFrame\": {edges[index]}, " +
                    $"\"endFrameExclusive\": {edges[index + 1]} }}");
                shots.Append(index + 1 < edges.Count - 1 ? ",\n" : "\n");
            }

            shots.Append("  ]\n}\n");
            File.WriteAllText(
                Path.Combine(generated, "shots.json"),
                shots.ToString(),
                new UTF8Encoding(false));
            return generated;
        }

        private TimelineAsset CreateTimeline(IReadOnlyList<int> edges)
        {
            return CreateTimeline(edges.Select(value => (double)value).ToArray());
        }

        private TimelineAsset CreateTimeline(IReadOnlyList<double> edgeFrames)
        {
            var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
            _timelines.Add(timeline);
            var track = timeline.CreateTrack<CinemachineTrack>(
                null,
                AICameraTimelinePreviewImporter.GeneratedCinemachineTrackName);
            for (var index = 0; index < edgeFrames.Count - 1; index++)
            {
                var clip = track.CreateDefaultClip();
                clip.start = edgeFrames[index] /
                    AICameraCutCorrectionRecorder.RequiredSampleRate;
                clip.duration =
                    (edgeFrames[index + 1] - edgeFrames[index]) /
                    AICameraCutCorrectionRecorder.RequiredSampleRate;
                clip.easeInDuration = 0d;
                clip.easeOutDuration = 0d;
            }

            return timeline;
        }

        private TimelineAsset CreateTimelineWithGap()
        {
            var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
            _timelines.Add(timeline);
            var track = timeline.CreateTrack<CinemachineTrack>(
                null,
                AICameraTimelinePreviewImporter.GeneratedCinemachineTrackName);
            var first = track.CreateDefaultClip();
            first.start = 0d;
            first.duration = 59d /
                AICameraCutCorrectionRecorder.RequiredSampleRate;
            first.easeInDuration = 0d;
            first.easeOutDuration = 0d;
            var second = track.CreateDefaultClip();
            second.start = 60d /
                AICameraCutCorrectionRecorder.RequiredSampleRate;
            second.duration = 660d /
                AICameraCutCorrectionRecorder.RequiredSampleRate;
            second.easeInDuration = 0d;
            second.easeOutDuration = 0d;
            return timeline;
        }
    }
}
