"""
Sound Block BIOS — Tests for the Virtual Machine bootstrap.

Run:
    cd afs-engine
    python3 tests/test_sb_bios.py -v
"""

import unittest
import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from afs.gpu.sb_fabric import gpu_available
from afs.gpu.sb_bios import SoundBlockBIOS
from afs.gpu.sb_spectral_cpu import SpectralCPU


@unittest.skipUnless(gpu_available(), "No GPU available")
class TestBIOSPost(unittest.TestCase):
    """POST: Power-On Self-Test."""

    def test_post_passes(self):
        """BIOS POST should detect GPU and verify primitives."""
        bios = SoundBlockBIOS()
        result = bios.post()
        self.assertTrue(result['gpu_available'])
        self.assertTrue(result['fabric_ok'])
        self.assertGreater(result['primitives_tested'], 0)
        self.assertGreater(result['vram_bytes'], 0)
        print(f"\n  BIOS POST:")
        print(f"    GPU: {result['adapter']}")
        print(f"    VRAM: {result['vram_mb']:.0f} MB")
        print(f"    Primitives tested: {result['primitives_tested']}")
        print(f"    POST time: {result['post_ms']:.1f}ms")


@unittest.skipUnless(gpu_available(), "No GPU available")
class TestBIOSMemoryMap(unittest.TestCase):
    """Memory map: the wide pipe between BIOS and OS."""

    def test_memory_map_created(self):
        """Memory map should allocate all regions."""
        bios = SoundBlockBIOS()
        bios.post()
        mem_map = bios.create_memory_map()

        self.assertGreater(len(mem_map), 0)
        for name in SoundBlockBIOS.REGION_RATIOS:
            self.assertIn(name, mem_map, f"Missing region: {name}")
            self.assertGreater(mem_map[name]['size'], 0)

        print(f"\n  {bios.print_memory_map()}")

    def test_memory_map_scales_with_vram(self):
        """Larger VRAM = larger regions."""
        bios = SoundBlockBIOS()
        bios.post()

        # Small budget
        small = bios.create_memory_map(vram_budget=8 * 1024 * 1024)  # 8MB
        small_gpu = small['gpu_workspace']['size']

        # Large budget
        large = bios.create_memory_map(vram_budget=256 * 1024 * 1024)  # 256MB
        large_gpu = large['gpu_workspace']['size']

        self.assertGreater(large_gpu, small_gpu)
        print(f"\n  Memory scaling:")
        print(f"    8MB VRAM:   gpu_workspace = {small_gpu:,} words ({small_gpu*4/1024:.0f} KB)")
        print(f"    256MB VRAM: gpu_workspace = {large_gpu:,} words ({large_gpu*4/1024:.0f} KB)")

    def test_regions_dont_overlap(self):
        """No two regions should occupy the same address space."""
        bios = SoundBlockBIOS()
        bios.post()
        bios.create_memory_map()

        regions = list(bios.memory_map.values())
        for i in range(len(regions)):
            for j in range(i + 1, len(regions)):
                ri = regions[i]
                rj = regions[j]
                end_i = ri['offset'] + ri['size']
                end_j = rj['offset'] + rj['size']
                # No overlap
                self.assertTrue(
                    end_i <= rj['offset'] or end_j <= ri['offset'],
                    f"Regions overlap: {ri} and {rj}")

    def test_aligned_to_1kb(self):
        """All regions should be 1KB aligned (256 words)."""
        bios = SoundBlockBIOS()
        bios.post()
        bios.create_memory_map()
        for name, info in bios.memory_map.items():
            self.assertEqual(info['size'] % 256, 0,
                f"Region '{name}' not 1KB aligned: size={info['size']}")


@unittest.skipUnless(gpu_available(), "No GPU available")
class TestBIOSBoot(unittest.TestCase):
    """Boot: initialize VM and hand off to Level 2."""

    def test_boot_succeeds(self):
        """BIOS boots successfully after POST."""
        bios = SoundBlockBIOS()
        bios.post()
        result = bios.boot()

        self.assertTrue(result['booted'])
        self.assertGreater(result['regions'], 0)
        self.assertGreater(result['total_mapped_kb'], 0)
        print(f"\n  BIOS Boot:")
        print(f"    VRAM: {result['total_vram_mb']:.0f} MB")
        print(f"    Mapped: {result['total_mapped_kb']:.0f} KB ({result['total_mapped_words']:,} words)")
        print(f"    Regions: {result['regions']}")
        print(f"    Boot time: {result['boot_ms']:.1f}ms")

    def test_boot_with_program(self):
        """Boot with a Level 2 program loaded."""
        E = SpectralCPU.encode
        program = [
            E(0x8, 1, imm=42),     # LOAD_IMM R1, 42
            E(0x8, 2, imm=58),     # LOAD_IMM R2, 58
            E(0x1, 3, 1, 2),       # ADD R3, R1, R2 = 100
            E(0xF),                 # HALT
        ]

        bios = SoundBlockBIOS()
        bios.post()
        boot_result = bios.boot(program=program)
        self.assertTrue(boot_result['booted'])

        # Run the program through the bridge
        cpu_result = bios.bridge.cpu_run(100)
        regs = bios.bridge.read_region('cpu_regs', 16)
        self.assertEqual(regs[3], 100)
        print(f"\n  Boot + run: R3 = {regs[3]} (42 + 58 = 100) ✓")

    def test_boot_without_post_fails(self):
        """Boot without POST should fail."""
        bios = SoundBlockBIOS()
        with self.assertRaises(RuntimeError):
            bios.boot()

    def test_full_bootstrap_sequence(self):
        """Complete bootstrap: POST → boot → run → result."""
        E = SpectralCPU.encode
        bios = SoundBlockBIOS()

        # POST
        post = bios.post()
        self.assertTrue(post['gpu_available'])

        # Boot with Fibonacci program
        program = [
            E(0x8, 1, imm=0),      # fib(n-2)
            E(0x8, 2, imm=1),      # fib(n-1)
            E(0x8, 3, imm=9),      # count
            E(0x8, 4, imm=1),
            E(0x8, 5, imm=0),
            E(0x1, 6, 1, 2),       # next = a + b
            E(0x1, 1, 2, 5),       # a = b
            E(0x1, 2, 6, 5),       # b = next
            E(0x2, 3, 3, 4),       # count--
            E(0xC, 0, 3, 5, imm=0xFFFB),  # BNE count, 0, -5
            E(0xF),                 # HALT
        ]

        boot = bios.boot(program=program)
        self.assertTrue(boot['booted'])

        # Run
        bios.bridge.cpu_run(200)
        regs = bios.bridge.read_region('cpu_regs', 16)

        self.assertEqual(regs[2], 55, f"Fibonacci(10) = {regs[2]}")
        print(f"\n  Full bootstrap: POST → boot → Fibonacci(10) = {regs[2]} ✓")
        print(f"    POST: {post['post_ms']:.1f}ms")
        print(f"    Boot: {boot['boot_ms']:.1f}ms")
        print(f"    Total: {post['post_ms'] + boot['boot_ms']:.1f}ms to running VM")


if __name__ == "__main__":
    unittest.main(verbosity=2)
