using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

namespace Streamingle.Editor
{
    public sealed class TimelineCameraDatasetExporterTests
    {
        [Test]
        public void SceneCameraSourceFingerprintNormalizesDependencyOrder()
        {
            var reordered = TimelineCameraDatasetExporter
                .ComposeSceneCameraSourceFingerprint(
                    "semantic-fingerprint",
                    new[]
                    {
                        "Assets\\Source\\B.anim|hash-b",
                        "Assets/Source/A.anim|hash-a",
                        "Assets/Source/B.anim|hash-b"
                    });
            var canonical = TimelineCameraDatasetExporter
                .ComposeSceneCameraSourceFingerprint(
                    "semantic-fingerprint",
                    new[]
                    {
                        "Assets/Source/A.anim|hash-a",
                        "Assets/Source/B.anim|hash-b"
                    });

            Assert.That(reordered, Is.EqualTo(canonical));
            Assert.That(reordered, Has.Length.EqualTo(64));
        }

        [Test]
        public void SceneCameraSourceFingerprintMatchingSupportsLegacyAndFailsClosed()
        {
            Assert.That(
                TimelineCameraDatasetExporter
                    .TryMatchSceneCameraSourceFingerprint(
                        TimelineCameraDatasetExporter
                            .LegacySourceFingerprintSchema,
                        "legacy-fingerprint",
                        "strong-fingerprint",
                        "legacy-fingerprint",
                        out var legacyAccepted,
                        out var legacyError),
                Is.True,
                legacyError);
            Assert.That(legacyAccepted, Is.True);

            Assert.That(
                TimelineCameraDatasetExporter
                    .TryMatchSceneCameraSourceFingerprint(
                        TimelineCameraDatasetExporter.SourceFingerprintSchema,
                        "strong-fingerprint",
                        "strong-fingerprint",
                        "legacy-fingerprint",
                        out legacyAccepted,
                        out var strongError),
                Is.True,
                strongError);
            Assert.That(legacyAccepted, Is.False);

            Assert.That(
                TimelineCameraDatasetExporter
                    .TryMatchSceneCameraSourceFingerprint(
                        TimelineCameraDatasetExporter.SourceFingerprintSchema,
                        "changed-fingerprint",
                        "strong-fingerprint",
                        "legacy-fingerprint",
                        out _,
                        out var mismatchError),
                Is.False);
            StringAssert.Contains("does not match", mismatchError);
        }

        [Test]
        public void GenerationInputExportDoesNotRequireAnAuthoredCamera()
        {
            var outputDirectory = Path.Combine(
                Path.GetTempPath(),
                "CWAI_GenerationInput_" + Guid.NewGuid().ToString("N"));
            var directorObject = new GameObject("GenerationInputDirector");
            var characterObject = new GameObject("Character");
            var hipsObject = new GameObject("Hips");
            var headObject = new GameObject("Head");
            var animator = characterObject.AddComponent<Animator>();
            hipsObject.transform.SetParent(characterObject.transform, false);
            headObject.transform.SetParent(hipsObject.transform, false);
            headObject.transform.localPosition = Vector3.up;

            var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
            timeline.name = "MotionAndAudioOnly";
            var motionClip = new AnimationClip { name = "Motion" };
            motionClip.SetCurve(
                "Hips",
                typeof(Transform),
                "m_LocalPosition.x",
                AnimationCurve.Linear(0f, 0f, 1f, 0.1f));
            motionClip.SetCurve(
                "Hips/Head",
                typeof(Transform),
                "m_LocalPosition.y",
                AnimationCurve.Linear(0f, 1f, 1f, 1.05f));
            var animationTrack = timeline.CreateTrack<AnimationTrack>();
            var animationTimelineClip =
                animationTrack.CreateClip<AnimationPlayableAsset>();
            ((AnimationPlayableAsset)animationTimelineClip.asset).clip = motionClip;
            animationTimelineClip.duration = 1.0;

            var audioClip = AudioClip.Create(
                "Audio",
                6000,
                1,
                6000,
                false);
            var audioSamples = Enumerable.Range(0, audioClip.samples)
                .Select(index => 0.25f * Mathf.Sin(index * 0.1f))
                .ToArray();
            Assert.That(audioClip.SetData(audioSamples, 0), Is.True);
            var audioTrack = timeline.CreateTrack<AudioTrack>();
            var audioTimelineClip = audioTrack.CreateClip<AudioPlayableAsset>();
            ((AudioPlayableAsset)audioTimelineClip.asset).clip = audioClip;
            audioTimelineClip.duration = 1.0;

            var director = directorObject.AddComponent<PlayableDirector>();
            director.playableAsset = timeline;
            director.SetGenericBinding(animationTrack, animator);
            directorObject.SetActive(false);

            try
            {
                TimelineCameraDatasetExporter
                    .ExportGenerationInput60FpsToForCli(
                        director,
                        outputDirectory,
                        false);

                Assert.That(directorObject.activeSelf, Is.False);

                var manifest = File.ReadAllText(Path.Combine(
                    outputDirectory,
                    "dataset_manifest.json"));
                StringAssert.Contains("\"inputKind\": \"generation_input\"", manifest);
                StringAssert.Contains("\"hasAuthoredCamera\": false", manifest);
                StringAssert.Contains("\"audioFingerprint\": \"", manifest);
                var songDirectory = Directory.GetDirectories(outputDirectory).Single();
                Assert.That(
                    new FileInfo(Path.Combine(songDirectory, "camera.f32")).Length,
                    Is.EqualTo(60L * 9 * sizeof(float)));
                Assert.That(
                    new FileInfo(Path.Combine(songDirectory, "shot_index.i32")).Length,
                    Is.EqualTo(60L * sizeof(int)));
                Assert.That(
                    new FileInfo(Path.Combine(songDirectory, "audio_features.f32")).Length,
                    Is.EqualTo(60L * 2 * sizeof(float)));

                using (var cameraReader = new BinaryReader(File.OpenRead(
                           Path.Combine(songDirectory, "camera.f32"))))
                {
                    Assert.That(float.IsNaN(cameraReader.ReadSingle()), Is.True);
                }

                using (var shotReader = new BinaryReader(File.OpenRead(
                           Path.Combine(songDirectory, "shot_index.i32"))))
                {
                    Assert.That(shotReader.ReadInt32(), Is.EqualTo(-1));
                }

                using (var featureReader = new BinaryReader(File.OpenRead(
                           Path.Combine(songDirectory, "audio_features.f32"))))
                {
                    var values = new float[120];
                    for (var index = 0; index < values.Length; index++)
                    {
                        values[index] = featureReader.ReadSingle();
                    }

                    Assert.That(values.All(float.IsFinite), Is.True);
                    Assert.That(values.Where((_, index) => index % 2 == 0).Max(),
                        Is.GreaterThan(0f));
                }

                var shots = File.ReadAllText(Path.Combine(songDirectory, "shots.json"));
                StringAssert.Contains("\"shots\": []", shots);
            }
            finally
            {
                if (Directory.Exists(outputDirectory))
                {
                    Directory.Delete(outputDirectory, true);
                }

                UnityEngine.Object.DestroyImmediate(audioClip);
                UnityEngine.Object.DestroyImmediate(motionClip);
                UnityEngine.Object.DestroyImmediate(timeline);
                UnityEngine.Object.DestroyImmediate(directorObject);
                UnityEngine.Object.DestroyImmediate(characterObject);
            }
        }

        [Test]
        public void GenerationInputSummaryReportsMissingSelection()
        {
            var summary = TimelineCameraDatasetExporter
                .GetGenerationInputSummaryForEditor(null);

            Assert.That(summary.isValid, Is.False);
            Assert.That(summary.error, Is.Not.Empty);
        }

        [Test]
        public void GenerationInputFingerprintRefusesDirtyScene()
        {
            var directorObject = new GameObject("DirtySourceDirector");
            var director = directorObject.AddComponent<PlayableDirector>();
            try
            {
                UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(
                    directorObject.scene);
                Assert.That(
                    TimelineCameraDatasetExporter
                        .TryComputeGenerationInputSourceFingerprint(
                            director,
                            out var fingerprint,
                            out var error),
                    Is.False);
                Assert.That(fingerprint, Is.Empty);
                StringAssert.Contains("unsaved changes", error);
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(directorObject);
            }
        }

        [Test]
        public void ReusableGenerationInputValidationAcceptsCompleteExport()
        {
            var root = CreateTemporaryDirectory();
            const string fingerprint =
                "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
                "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
            try
            {
                var candidate = CreateReusableGenerationInputFixture(
                    root,
                    "GenerationInput_60fps_20260802_120000_000",
                    fingerprint);

                Assert.That(
                    TimelineCameraDatasetExporter
                        .TryValidateReusableGenerationInputExport(
                            candidate,
                            fingerprint,
                            out var error),
                    Is.True,
                    error);
            }
            finally
            {
                Directory.Delete(root, true);
            }
        }

        [Test]
        public void ReusableGenerationInputValidationRejectsChangedSourceAndAudio()
        {
            var root = CreateTemporaryDirectory();
            const string fingerprint =
                "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +
                "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
            try
            {
                var candidate = CreateReusableGenerationInputFixture(
                    root,
                    "GenerationInput_60fps_20260802_120000_000",
                    fingerprint);

                Assert.That(
                    TimelineCameraDatasetExporter
                        .TryValidateReusableGenerationInputExport(
                            candidate,
                            "changed-source",
                            out _),
                    Is.False);

                File.AppendAllText(
                    Path.Combine(candidate, "01_Test", "audio_source.wav"),
                    "tampered");
                Assert.That(
                    TimelineCameraDatasetExporter
                        .TryValidateReusableGenerationInputExport(
                            candidate,
                            fingerprint,
                            out var error),
                    Is.False);
                StringAssert.Contains("fingerprint", error);
            }
            finally
            {
                Directory.Delete(root, true);
            }
        }

        [Test]
        public void ReusableGenerationInputSearchSkipsIncompleteNewestCandidate()
        {
            var root = CreateTemporaryDirectory();
            const string fingerprint =
                "cccccccccccccccccccccccccccccccc" +
                "cccccccccccccccccccccccccccccccc";
            try
            {
                var expected = CreateReusableGenerationInputFixture(
                    root,
                    "GenerationInput_60fps_20260802_120000_000",
                    fingerprint);
                var incomplete = CreateReusableGenerationInputFixture(
                    root,
                    "GenerationInput_60fps_20260802_120001_000",
                    fingerprint);
                File.Delete(Path.Combine(
                    incomplete,
                    "01_Test",
                    "root.f32"));

                Assert.That(
                    TimelineCameraDatasetExporter
                        .TryFindReusableGenerationInputExport(
                            root,
                            fingerprint,
                            out var actual,
                            out var error),
                    Is.True,
                    error);
                Assert.That(actual, Is.EqualTo(Path.GetFullPath(expected)));
            }
            finally
            {
                Directory.Delete(root, true);
            }
        }

        [Test]
        public void ReusableGenerationInputValidationRejectsSameSizeBinaryCorruption()
        {
            var root = CreateTemporaryDirectory();
            const string fingerprint =
                "dddddddddddddddddddddddddddddddd" +
                "dddddddddddddddddddddddddddddddd";
            try
            {
                var candidate = CreateReusableGenerationInputFixture(
                    root,
                    "GenerationInput_60fps_20260802_120000_000",
                    fingerprint);
                var jointsPath = Path.Combine(
                    candidate,
                    "01_Test",
                    "joints_world.f32");
                var bytes = File.ReadAllBytes(jointsPath);
                bytes[0] ^= 0x01;
                File.WriteAllBytes(jointsPath, bytes);

                Assert.That(
                    TimelineCameraDatasetExporter
                        .TryValidateReusableGenerationInputExport(
                            candidate,
                            fingerprint,
                            out var error),
                    Is.False);
                StringAssert.Contains("fingerprint", error);
            }
            finally
            {
                Directory.Delete(root, true);
            }
        }

        [Test]
        public void ReusableGenerationInputSearchFailsClosedOnMalformedManifest()
        {
            var root = CreateTemporaryDirectory();
            try
            {
                var candidate = Path.Combine(
                    root,
                    "GenerationInput_60fps_20260802_120000_000");
                Directory.CreateDirectory(candidate);
                File.WriteAllText(
                    Path.Combine(candidate, "dataset_manifest.json"),
                    "{");

                Assert.That(
                    TimelineCameraDatasetExporter
                        .TryFindReusableGenerationInputExport(
                            root,
                            "source",
                            out var actual,
                            out _),
                    Is.False);
                Assert.That(actual, Is.Empty);
            }
            finally
            {
                Directory.Delete(root, true);
            }
        }

        private static string CreateTemporaryDirectory()
        {
            var path = Path.Combine(
                Path.GetTempPath(),
                "CWAI_ReusableGenerationInput_" + Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(path);
            return path;
        }

        private static string CreateReusableGenerationInputFixture(
            string root,
            string directoryName,
            string sourceFingerprint)
        {
            const int frameCount = 2;
            const int boneCount = 2;
            var candidate = Path.Combine(root, directoryName);
            var songDirectory = Path.Combine(candidate, "01_Test");
            Directory.CreateDirectory(songDirectory);

            var audioPath = Path.Combine(songDirectory, "audio_source.wav");
            File.WriteAllBytes(audioPath, new byte[] { 1, 2, 3, 4, 5 });
            var audioFingerprint = ComputeSha256(audioPath);
            var song = new GenerationInputSongFixture
            {
                songName = "Test",
                role = "generation_input",
                inputKind = "generation_input",
                hasAuthoredCamera = false,
                folderName = "01_Test",
                audioFile = "audio_source.wav",
                audioFingerprint = audioFingerprint,
                sampleRate = 60,
                frameCount = frameCount,
                sharedBoneCount = boneCount,
                shotCount = 0,
                missingCameraFrames = frameCount,
                missingJointSamples = 0,
                missingRootSamples = 0,
                jointsFile = "joints_world.f32",
                cameraFile = "camera.f32",
                rootFile = "root.f32",
                shotIndexFile = "shot_index.i32",
                timeFile = "time.f64",
                shotsFile = "shots.json",
                previewFile = "preview.csv",
                audioFeaturesFile = "audio_features.f32"
            };
            var manifest = new GenerationInputManifestFixture
            {
                schemaVersion = "1.3",
                role = "generation_input",
                inputKind = "generation_input",
                hasAuthoredCamera = false,
                sampleRate = 60,
                sourceFingerprint = sourceFingerprint,
                discoveredCameraTrackCount = 0,
                exportedCameraTrackCount = 0,
                skippedCameraTracks = new List<string>(),
                sharedBones = new List<string> { "Hips", "Head" },
                songs = new List<GenerationInputSongFixture> { song }
            };

            File.WriteAllText(
                Path.Combine(songDirectory, "shots.json"),
                JsonUtility.ToJson(new ShotFileFixture
                {
                    timelineName = "Test",
                    shots = new List<ShotFixture>()
                }, true));
            File.WriteAllText(
                Path.Combine(songDirectory, "preview.csv"),
                "frame,time\n0,0\n");

            WriteZeroBytes(
                Path.Combine(songDirectory, song.jointsFile),
                frameCount * boneCount * 3 * sizeof(float));
            WriteZeroBytes(
                Path.Combine(songDirectory, song.rootFile),
                frameCount * 14 * sizeof(float));
            WriteZeroBytes(
                Path.Combine(songDirectory, song.timeFile),
                frameCount * sizeof(double));
            WriteZeroBytes(
                Path.Combine(songDirectory, song.audioFeaturesFile),
                frameCount * 2 * sizeof(float));

            using (var writer = new BinaryWriter(File.Create(
                       Path.Combine(songDirectory, song.cameraFile))))
            {
                for (var index = 0; index < frameCount * 9; index++)
                {
                    writer.Write(float.NaN);
                }
            }

            using (var writer = new BinaryWriter(File.Create(
                       Path.Combine(songDirectory, song.shotIndexFile))))
            {
                for (var index = 0; index < frameCount; index++)
                {
                    writer.Write(-1);
                }
            }

            song.jointsFingerprint = ComputeSha256(
                Path.Combine(songDirectory, song.jointsFile));
            song.cameraFingerprint = ComputeSha256(
                Path.Combine(songDirectory, song.cameraFile));
            song.rootFingerprint = ComputeSha256(
                Path.Combine(songDirectory, song.rootFile));
            song.shotIndexFingerprint = ComputeSha256(
                Path.Combine(songDirectory, song.shotIndexFile));
            song.timeFingerprint = ComputeSha256(
                Path.Combine(songDirectory, song.timeFile));
            song.audioFeaturesFingerprint = ComputeSha256(
                Path.Combine(songDirectory, song.audioFeaturesFile));
            File.WriteAllText(
                Path.Combine(candidate, "dataset_manifest.json"),
                JsonUtility.ToJson(manifest, true));
            File.WriteAllText(
                Path.Combine(songDirectory, "metadata.json"),
                JsonUtility.ToJson(song, true));

            return candidate;
        }

        private static void WriteZeroBytes(string path, int count)
        {
            File.WriteAllBytes(path, new byte[count]);
        }

        private static string ComputeSha256(string path)
        {
            using var stream = File.OpenRead(path);
            using var sha256 = SHA256.Create();
            return string.Concat(
                sha256.ComputeHash(stream)
                    .Select(value => value.ToString("x2")));
        }

        [Serializable]
        private sealed class GenerationInputManifestFixture
        {
            public string schemaVersion;
            public string role;
            public string inputKind;
            public bool hasAuthoredCamera;
            public int sampleRate;
            public string sourceFingerprint;
            public int discoveredCameraTrackCount;
            public int exportedCameraTrackCount;
            public List<string> skippedCameraTracks;
            public List<string> sharedBones;
            public List<GenerationInputSongFixture> songs;
        }

        [Serializable]
        private sealed class GenerationInputSongFixture
        {
            public string songName;
            public string role;
            public string inputKind;
            public bool hasAuthoredCamera;
            public string folderName;
            public string audioFile;
            public string audioFingerprint;
            public int sampleRate;
            public int frameCount;
            public int sharedBoneCount;
            public int shotCount;
            public int missingCameraFrames;
            public int missingJointSamples;
            public int missingRootSamples;
            public string jointsFile;
            public string jointsFingerprint;
            public string cameraFile;
            public string cameraFingerprint;
            public string rootFile;
            public string rootFingerprint;
            public string shotIndexFile;
            public string shotIndexFingerprint;
            public string timeFile;
            public string timeFingerprint;
            public string shotsFile;
            public string previewFile;
            public string audioFeaturesFile;
            public string audioFeaturesFingerprint;
        }

        [Serializable]
        private sealed class ShotFileFixture
        {
            public string timelineName;
            public List<ShotFixture> shots;
        }

        [Serializable]
        private sealed class ShotFixture
        {
        }
    }
}
