using System;
using NUnit.Framework;
using VMdemo.Simulation;

namespace VMdemo.Tests.EditMode
{
    public class Step4PageTableAndFifoTests
    {
        [Test]
        public void TwoLevelPageTable_SetPresent_ThenTryGetPresentPfn_Hits()
        {
            var pageTable = new TwoLevelPageTable(3, 2);
            pageTable.SetPresent(5UL, 3UL, 9UL, dirty: true);

            var hit = pageTable.TryGetPresentPfn(5UL, 3UL, out var pfn);
            var exists = pageTable.TryGetEntry(5UL, 3UL, out var entry);

            Assert.IsTrue(hit);
            Assert.That(pfn, Is.EqualTo(9UL));
            Assert.IsTrue(exists);
            Assert.That(entry.Present, Is.True);
            Assert.That(entry.Dirty, Is.True);
        }

        [Test]
        public void TwoLevelPageTable_MarkNotPresent_ThenMisses()
        {
            var pageTable = new TwoLevelPageTable(3, 2);
            pageTable.SetPresent(1UL, 2UL, 7UL);

            var changed = pageTable.MarkNotPresent(1UL, 2UL);
            var changedAgain = pageTable.MarkNotPresent(1UL, 2UL);
            var hitAfterMark = pageTable.TryGetPresentPfn(1UL, 2UL, out _);
            var exists = pageTable.TryGetEntry(1UL, 2UL, out var entry);

            Assert.IsTrue(changed);
            Assert.IsFalse(changedAgain);
            Assert.IsFalse(hitAfterMark);
            Assert.IsTrue(exists);
            Assert.IsFalse(entry.Present);
        }

        [Test]
        public void PhysicalMemoryManager_AllocateForVpn_EvictsInFifoOrder()
        {
            var manager = new PhysicalMemoryManager(2UL);

            var r1 = manager.AllocateForVpn(1UL);
            var r2 = manager.AllocateForVpn(2UL);
            var r3 = manager.AllocateForVpn(3UL); // evict 1
            var r4 = manager.AllocateForVpn(4UL); // evict 2

            Assert.That(r1.AssignedPfn, Is.EqualTo(0UL));
            Assert.That(r2.AssignedPfn, Is.EqualTo(1UL));
            Assert.That(r3.AssignedPfn, Is.EqualTo(0UL));
            Assert.That(r4.AssignedPfn, Is.EqualTo(1UL));

            Assert.IsFalse(r1.Eviction.HasEvicted);
            Assert.IsFalse(r2.Eviction.HasEvicted);
            Assert.IsTrue(r3.Eviction.HasEvicted);
            Assert.That(r3.Eviction.EvictedVpn, Is.EqualTo(1UL));
            Assert.That(r3.Eviction.EvictedPfn, Is.EqualTo(0UL));
            Assert.IsTrue(r4.Eviction.HasEvicted);
            Assert.That(r4.Eviction.EvictedVpn, Is.EqualTo(2UL));
            Assert.That(r4.Eviction.EvictedPfn, Is.EqualTo(1UL));

            var fifoOrder = manager.GetFifoVpnOrder();
            Assert.That(fifoOrder.Count, Is.EqualTo(2));
            Assert.That(fifoOrder[0], Is.EqualTo(3UL));
            Assert.That(fifoOrder[1], Is.EqualTo(4UL));
        }

        [Test]
        public void Resolve_WhenEvictionHappens_SyncsPageTableAndTlb()
        {
            var pageTable = new TwoLevelPageTable(4, 4);
            var memory = new PhysicalMemoryManager(1UL);
            var tlb = new TlbCache(4);

            const ulong vpn1 = 1UL;
            pageTable.GetIndicesFromVpn(vpn1, out var l1_1, out var l2_1);
            var first = MemoryAccessResolver.Resolve(vpn1, l1_1, l2_1, pageTable, memory, tlb);

            Assert.IsTrue(first.PageFault);
            Assert.IsFalse(first.PageTableHit);
            Assert.IsFalse(first.Eviction.HasEvicted);
            Assert.IsTrue(tlb.Lookup(vpn1, out _));

            var second = MemoryAccessResolver.Resolve(vpn1, l1_1, l2_1, pageTable, memory, tlb);
            Assert.IsFalse(second.PageFault);
            Assert.IsTrue(second.PageTableHit);

            const ulong vpn2 = 2UL;
            pageTable.GetIndicesFromVpn(vpn2, out var l1_2, out var l2_2);
            var third = MemoryAccessResolver.Resolve(vpn2, l1_2, l2_2, pageTable, memory, tlb);

            Assert.IsTrue(third.PageFault);
            Assert.IsTrue(third.Eviction.HasEvicted);
            Assert.That(third.Eviction.EvictedVpn, Is.EqualTo(vpn1));
            Assert.IsFalse(tlb.Lookup(vpn1, out _));
            Assert.IsTrue(tlb.Lookup(vpn2, out _));

            pageTable.GetIndicesFromVpn(vpn1, out var evictedL1, out var evictedL2);
            Assert.IsFalse(pageTable.TryGetPresentPfn(evictedL1, evictedL2, out _));
            Assert.IsFalse(memory.TryGetResidentPfn(vpn1, out _));
            Assert.IsTrue(memory.TryGetResidentPfn(vpn2, out _));
        }

        [Test]
        public void HighVolumeAccess_N10000_RunsWithoutStateCorruption()
        {
            var pageTable = new TwoLevelPageTable(8, 8);
            var memory = new PhysicalMemoryManager(16UL);
            var tlb = new TlbCache(8);
            var random = new Random(123);

            for (var i = 0; i < 10000; i++)
            {
                var vpn = (ulong)random.Next(0, 512);
                pageTable.GetIndicesFromVpn(vpn, out var l1, out var l2);

                var result = MemoryAccessResolver.Resolve(vpn, l1, l2, pageTable, memory, tlb);
                var fifoOrder = memory.GetFifoVpnOrder();

                Assert.That(memory.ResidentCount, Is.LessThanOrEqualTo(16));
                Assert.That(fifoOrder.Count, Is.EqualTo(memory.ResidentCount));
                if (result.PageTableHit)
                {
                    Assert.IsFalse(result.PageFault);
                }
            }
        }
    }
}
