package cpuid

import (
	"archive/zip"
	"fmt"
	"io/ioutil"
	"math"
	"path/filepath"
	"sort"
	"strings"
	"testing"
)

type fakecpuid map[uint32][][]uint32

type idfuncs struct {
	cpuid   func(op uint32) (eax, ebx, ecx, edx uint32)
	cpuidex func(op, op2 uint32) (eax, ebx, ecx, edx uint32)
	xgetbv  func(index uint32) (eax, edx uint32)
}

func (f fakecpuid) String() string {
	var out = make([]string, 0, len(f))
	for key, val := range f {
		for _, v := range val {
			out = append(out, fmt.Sprintf("CPUID %08x: [%08x, %08x, %08x, %08x]", key, v[0], v[1], v[2], v[3]))
		}
	}
	sorter := sort.StringSlice(out)
	sort.Sort(&sorter)
	return strings.Join(sorter, "\n")
}

func mockCPU(def []byte) func() {
	lines := strings.Split(string(def), "\n")
	anyfound := false
	fakeID := make(fakecpuid)
	for _, line := range lines {
		line = strings.Trim(line, "\r\t ")
		if !strings.HasPrefix(line, "CPUID") {
			continue
		}
		// Only collect for first cpu
		if strings.HasPrefix(line, "CPUID 00000000") {
			if anyfound {
				break
			}
		}
		//if !strings.Contains(line, "-") {
		//	continue
		//}
		items := strings.Split(line, ":")
		if len(items) < 2 {
			if len(line) == 51 || len(line) == 50 {
				items = []string{line[0:14], line[15:]}
			} else {
				items = strings.Split(line, "\t")
				if len(items) != 2 {
					//fmt.Println("not found:", line, "len:", len(line))
					continue
				}
			}
		}
		items = items[0:2]
		vals := strings.Trim(items[1], "\r\n ")

		var idV uint32
		n, err := fmt.Sscanf(items[0], "CPUID %x", &idV)
		if err != nil || n != 1 {
			continue
		}
		existing, ok := fakeID[idV]
		if !ok {
			existing = make([][]uint32, 0)
		}

		values := make([]uint32, 4)
		n, err = fmt.Sscanf(vals, "%x-%x-%x-%x", &values[0], &values[1], &values[2], &values[3])
		if n != 4 || err != nil {
			n, err = fmt.Sscanf(vals, "%x %x %x %x", &values[0], &values[1], &values[2], &values[3])
			if n != 4 || err != nil {
				//fmt.Println("scanned", vals, "got", n, "Err:", err)
				continue
			}
		}

		existing = append(existing, values)
		fakeID[idV] = existing
		anyfound = true
	}

	restorer := func(f idfuncs) func() {
		return func() {
			cpuid = f.cpuid
			cpuidex = f.cpuidex
			xgetbv = f.xgetbv
		}
	}(idfuncs{cpuid: cpuid, cpuidex: cpuidex, xgetbv: xgetbv})

	cpuid = func(op uint32) (eax, ebx, ecx, edx uint32) {
		if op == 0x80000000 || op == 0 {
			var ok bool
			_, ok = fakeID[op]
			if !ok {
				return 0, 0, 0, 0
			}
		}
		first, ok := fakeID[op]
		if !ok {
			if op > maxFunctionID() {
				panic(fmt.Sprintf("Base not found: %v, request:%#v\n", fakeID, op))
			} else {
				// we have some entries missing
				return 0, 0, 0, 0
			}
		}
		theid := first[0]
		return theid[0], theid[1], theid[2], theid[3]
	}
	cpuidex = func(op, op2 uint32) (eax, ebx, ecx, edx uint32) {
		if op == 0x80000000 {
			var ok bool
			_, ok = fakeID[op]
			if !ok {
				return 0, 0, 0, 0
			}
		}
		first, ok := fakeID[op]
		if !ok {
			if op > maxExtendedFunction() {
				panic(fmt.Sprintf("Extended not found Info: %v, request:%#v, %#v\n", fakeID, op, op2))
			} else {
				// we have some entries missing
				return 0, 0, 0, 0
			}
		}
		if int(op2) >= len(first) {
			//fmt.Printf("Extended not found Info: %v, request:%#v, %#v\n", fakeID, op, op2)
			return 0, 0, 0, 0
		}
		theid := first[op2]
		return theid[0], theid[1], theid[2], theid[3]
	}
	xgetbv = func(index uint32) (eax, edx uint32) {
		first, ok := fakeID[1]
		if !ok {
			panic(fmt.Sprintf("XGETBV not supported %v", fakeID))
		}
		second := first[0]
		// ECX bit 26 must be set
		if (second[2] & 1 << 26) == 0 {
			panic(fmt.Sprintf("XGETBV not supported %v", fakeID))
		}
		// We don't have any data to return, unfortunately
		return math.MaxUint32, math.MaxUint32
	}
	return restorer
}

func TestMocks(t *testing.T) {
	zr, err := zip.OpenReader("testdata/cpuid_data.zip")
	if err != nil {
		t.Skip("No testdata:", err)
	}
	defer zr.Close()
	for _, f := range zr.File {
		t.Run(filepath.Base(f.Name), func(t *testing.T) {
			rc, err := f.Open()
			if err != nil {
				t.Fatal(err)
			}
			content, err := ioutil.ReadAll(rc)
			if err != nil {
				t.Fatal(err)
			}
			rc.Close()
			t.Log("Opening", f.FileInfo().Name())
			restore := mockCPU(content)
			Detect()
			t.Log("Name:", CPU.BrandName)
			n := maxFunctionID()
			t.Logf("Max Function:0x%x", n)
			n = maxExtendedFunction()
			t.Logf("Max Extended Function:0x%x", n)
			t.Log("VendorString:", CPU.VendorString)
			t.Log("VendorID:", CPU.VendorID)
			t.Log("PhysicalCores:", CPU.PhysicalCores)
			t.Log("ThreadsPerCore:", CPU.ThreadsPerCore)
			t.Log("LogicalCores:", CPU.LogicalCores)
			t.Log("Family", CPU.Family, "Model:", CPU.Model, "Stepping:", CPU.Stepping)
			t.Log("Features:", strings.Join(CPU.FeatureSet(), ","))
			t.Log("Microarchitecture level:", CPU.X64Level())
			t.Log("Cacheline bytes:", CPU.CacheLine)
			t.Log("L1 Instruction Cache:", CPU.Cache.L1I, "bytes")
			t.Log("L1 Data Cache:", CPU.Cache.L1D, "bytes")
			t.Log("L2 Cache:", CPU.Cache.L2, "bytes")
			t.Log("L3 Cache:", CPU.Cache.L3, "bytes")
			t.Log("Hz:", CPU.Hz, "Hz")
			t.Log("Boost:", CPU.BoostFreq, "Hz")
			if CPU.LogicalCores > 0 && CPU.PhysicalCores > 0 {
				if CPU.LogicalCores != CPU.PhysicalCores*CPU.ThreadsPerCore {
					t.Fatalf("Core count mismatch, LogicalCores (%d) != PhysicalCores (%d) * CPU.ThreadsPerCore (%d)",
						CPU.LogicalCores, CPU.PhysicalCores, CPU.ThreadsPerCore)
				}
			}

			if CPU.ThreadsPerCore > 1 && !CPU.Supports(HTT) {
				t.Fatalf("Hyperthreading not detected")
			}
			if CPU.ThreadsPerCore == 1 && CPU.Supports(HTT) {
				t.Fatalf("Hyperthreading detected, but only 1 Thread per core")
			}
			restore()
		})
	}

	Detect()

}
