package server

import (
	"bytes"
	"encoding/binary"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/ollama/ollama/template"
	"github.com/ollama/ollama/types/model"
)

// Constants for GGUF magic bytes and version
var (
	ggufMagic = []byte{0x47, 0x47, 0x55, 0x46} // "GGUF"
	ggufVer   = uint32(3)                      // Version 3
)

// Helper function to create mock GGUF data
func createMockGGUFData(architecture string, vision bool) []byte {
	var buf bytes.Buffer

	// Write GGUF header
	buf.Write(ggufMagic)
	binary.Write(&buf, binary.LittleEndian, ggufVer)

	// Write tensor count (0 for our test)
	var numTensors uint64 = 0
	binary.Write(&buf, binary.LittleEndian, numTensors)

	// Calculate number of metadata entries
	numMetaEntries := uint64(1) // architecture entry
	if vision {
		numMetaEntries++
	}
	// Add embedding entry if architecture is "bert"
	if architecture == "bert" {
		numMetaEntries++
	}
	binary.Write(&buf, binary.LittleEndian, numMetaEntries)

	// Write architecture metadata
	archKey := "general.architecture"
	keyLen := uint64(len(archKey))
	binary.Write(&buf, binary.LittleEndian, keyLen)
	buf.WriteString(archKey)

	// String type (8)
	var strType uint32 = 8
	binary.Write(&buf, binary.LittleEndian, strType)

	// String length
	strLen := uint64(len(architecture))
	binary.Write(&buf, binary.LittleEndian, strLen)
	buf.WriteString(architecture)

	if vision {
		visionKey := architecture + ".vision.block_count"
		keyLen = uint64(len(visionKey))
		binary.Write(&buf, binary.LittleEndian, keyLen)
		buf.WriteString(visionKey)

		// uint32 type (4)
		var uint32Type uint32 = 4
		binary.Write(&buf, binary.LittleEndian, uint32Type)

		// uint32 value (1)
		var countVal uint32 = 1
		binary.Write(&buf, binary.LittleEndian, countVal)
	}
	// Write embedding metadata if architecture is "bert"
	if architecture == "bert" {
		poolKey := architecture + ".pooling_type"
		keyLen = uint64(len(poolKey))
		binary.Write(&buf, binary.LittleEndian, keyLen)
		buf.WriteString(poolKey)

		// uint32 type (4)
		var uint32Type uint32 = 4
		binary.Write(&buf, binary.LittleEndian, uint32Type)

		// uint32 value (1)
		var poolingVal uint32 = 1
		binary.Write(&buf, binary.LittleEndian, poolingVal)
	}

	return buf.Bytes()
}

func TestModelCapabilities(t *testing.T) {
	// Create a temporary directory for test files
	tempDir := t.TempDir()

	// Create different types of mock model files
	completionModelPath := filepath.Join(tempDir, "model.bin")
	visionModelPath := filepath.Join(tempDir, "vision_model.bin")
	embeddingModelPath := filepath.Join(tempDir, "embedding_model.bin")
	// Create a simple model file for tests that don't depend on GGUF content
	simpleModelPath := filepath.Join(tempDir, "simple_model.bin")

	if err := errors.Join(
		os.WriteFile(completionModelPath, createMockGGUFData("llama", false), 0o644),
		os.WriteFile(visionModelPath, createMockGGUFData("llama", true), 0o644),
		os.WriteFile(embeddingModelPath, createMockGGUFData("bert", false), 0o644),
		os.WriteFile(simpleModelPath, []byte("dummy model data"), 0o644),
	); err != nil {
		t.Fatalf("Failed to create model files: %v", err)
	}

	toolsInsertTemplate, err := template.Parse("{{ .prompt }}{{ if .tools }}{{ .tools }}{{ end }}{{ if .suffix }}{{ .suffix }}{{ end }}")
	if err != nil {
		t.Fatalf("Failed to parse template: %v", err)
	}
	chatTemplate, err := template.Parse("{{ .prompt }}")
	if err != nil {
		t.Fatalf("Failed to parse template: %v", err)
	}
	toolsTemplate, err := template.Parse("{{ .prompt }}{{ if .tools }}{{ .tools }}{{ end }}")
	if err != nil {
		t.Fatalf("Failed to parse template: %v", err)
	}

	testModels := []struct {
		name         string
		model        Model
		expectedCaps []model.Capability
	}{
		{
			name: "model with completion capability",
			model: Model{
				ModelPath: completionModelPath,
				Template:  chatTemplate,
			},
			expectedCaps: []model.Capability{model.CapabilityCompletion},
		},

		{
			name: "model with completion, tools, and insert capability",
			model: Model{
				ModelPath: completionModelPath,
				Template:  toolsInsertTemplate,
			},
			expectedCaps: []model.Capability{model.CapabilityCompletion, model.CapabilityTools, model.CapabilityInsert},
		},
		{
			name: "model with tools and insert capability",
			model: Model{
				ModelPath: simpleModelPath,
				Template:  toolsInsertTemplate,
			},
			expectedCaps: []model.Capability{model.CapabilityTools, model.CapabilityInsert},
		},
		{
			name: "model with tools capability",
			model: Model{
				ModelPath: simpleModelPath,
				Template:  toolsTemplate,
			},
			expectedCaps: []model.Capability{model.CapabilityTools},
		},
		{
			name: "model with vision capability",
			model: Model{
				ModelPath: visionModelPath,
				Template:  chatTemplate,
			},
			expectedCaps: []model.Capability{model.CapabilityCompletion, model.CapabilityVision},
		},
		{
			name: "model with vision, tools, and insert capability",
			model: Model{
				ModelPath: visionModelPath,
				Template:  toolsInsertTemplate,
			},
			expectedCaps: []model.Capability{model.CapabilityCompletion, model.CapabilityVision, model.CapabilityTools, model.CapabilityInsert},
		},
		{
			name: "model with embedding capability",
			model: Model{
				ModelPath: embeddingModelPath,
				Template:  chatTemplate,
			},
			expectedCaps: []model.Capability{model.CapabilityEmbedding},
		},
	}

	// compare two slices of model.Capability regardless of order
	compareCapabilities := func(a, b []model.Capability) bool {
		if len(a) != len(b) {
			return false
		}

		aCount := make(map[model.Capability]int)
		for _, cap := range a {
			aCount[cap]++
		}

		bCount := make(map[model.Capability]int)
		for _, cap := range b {
			bCount[cap]++
		}

		for cap, count := range aCount {
			if bCount[cap] != count {
				return false
			}
		}

		return true
	}

	for _, tt := range testModels {
		t.Run(tt.name, func(t *testing.T) {
			// Test Capabilities method
			caps := tt.model.Capabilities()
			if !compareCapabilities(caps, tt.expectedCaps) {
				t.Errorf("Expected capabilities %v, got %v", tt.expectedCaps, caps)
			}
		})
	}
}

func TestModelCheckCapabilities(t *testing.T) {
	// Create a temporary directory for test files
	tempDir := t.TempDir()

	visionModelPath := filepath.Join(tempDir, "vision_model.bin")
	simpleModelPath := filepath.Join(tempDir, "model.bin")
	embeddingModelPath := filepath.Join(tempDir, "embedding_model.bin")

	if err := errors.Join(
		os.WriteFile(simpleModelPath, []byte("dummy model data"), 0o644),
		os.WriteFile(visionModelPath, createMockGGUFData("llama", true), 0o644),
		os.WriteFile(embeddingModelPath, createMockGGUFData("bert", false), 0o644),
	); err != nil {
		t.Fatalf("Failed to create model files: %v", err)
	}

	toolsInsertTemplate, err := template.Parse("{{ .prompt }}{{ if .tools }}{{ .tools }}{{ end }}{{ if .suffix }}{{ .suffix }}{{ end }}")
	if err != nil {
		t.Fatalf("Failed to parse template: %v", err)
	}
	chatTemplate, err := template.Parse("{{ .prompt }}")
	if err != nil {
		t.Fatalf("Failed to parse template: %v", err)
	}
	toolsTemplate, err := template.Parse("{{ .prompt }}{{ if .tools }}{{ .tools }}{{ end }}")
	if err != nil {
		t.Fatalf("Failed to parse template: %v", err)
	}

	tests := []struct {
		name           string
		model          Model
		checkCaps      []model.Capability
		expectedErrMsg string
	}{
		{
			name: "completion model without tools capability",
			model: Model{
				ModelPath: simpleModelPath,
				Template:  chatTemplate,
			},
			checkCaps:      []model.Capability{model.CapabilityTools},
			expectedErrMsg: "does not support tools",
		},
		{
			name: "model with all needed capabilities",
			model: Model{
				ModelPath: simpleModelPath,
				Template:  toolsInsertTemplate,
			},
			checkCaps: []model.Capability{model.CapabilityTools, model.CapabilityInsert},
		},
		{
			name: "model missing insert capability",
			model: Model{
				ModelPath: simpleModelPath,
				Template:  toolsTemplate,
			},
			checkCaps:      []model.Capability{model.CapabilityInsert},
			expectedErrMsg: "does not support insert",
		},
		{
			name: "model missing vision capability",
			model: Model{
				ModelPath: simpleModelPath,
				Template:  toolsTemplate,
			},
			checkCaps:      []model.Capability{model.CapabilityVision},
			expectedErrMsg: "does not support vision",
		},
		{
			name: "model with vision capability",
			model: Model{
				ModelPath: visionModelPath,
				Template:  chatTemplate,
			},
			checkCaps: []model.Capability{model.CapabilityVision},
		},
		{
			name: "model with embedding capability",
			model: Model{
				ModelPath: embeddingModelPath,
				Template:  chatTemplate,
			},
			checkCaps: []model.Capability{model.CapabilityEmbedding},
		},
		{
			name: "unknown capability",
			model: Model{
				ModelPath: simpleModelPath,
				Template:  chatTemplate,
			},
			checkCaps:      []model.Capability{"unknown"},
			expectedErrMsg: "unknown capability",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Test CheckCapabilities method
			err := tt.model.CheckCapabilities(tt.checkCaps...)
			if tt.expectedErrMsg == "" {
				if err != nil {
					t.Errorf("Expected no error, got: %v", err)
				}
			} else {
				if err == nil {
					t.Errorf("Expected error containing %q, got nil", tt.expectedErrMsg)
				} else if !strings.Contains(err.Error(), tt.expectedErrMsg) {
					t.Errorf("Expected error containing %q, got: %v", tt.expectedErrMsg, err)
				}
			}
		})
	}
}
