package server

import (
	"bytes"
	"cmp"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"io"
	"maps"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"reflect"
	"slices"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	gocmp "github.com/google/go-cmp/cmp"
	gocmpopts "github.com/google/go-cmp/cmp/cmpopts"

	"github.com/ollama/ollama/api"
	"github.com/ollama/ollama/convert"
	"github.com/ollama/ollama/envconfig"
	"github.com/ollama/ollama/fs/ggml"
	"github.com/ollama/ollama/manifest"
	"github.com/ollama/ollama/types/model"
)

var stream bool = false

func createBinFile(t *testing.T, kv map[string]any, ti []*ggml.Tensor) (string, string) {
	t.Helper()
	t.Setenv("OLLAMA_MODELS", cmp.Or(os.Getenv("OLLAMA_MODELS"), t.TempDir()))

	modelDir := envconfig.Models()

	f, err := os.CreateTemp(t.TempDir(), "")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()

	var base convert.KV = map[string]any{"general.architecture": "test"}
	maps.Copy(base, kv)

	if err := ggml.WriteGGUF(f, base, ti); err != nil {
		t.Fatal(err)
	}
	// Calculate sha256 of file
	if _, err := f.Seek(0, 0); err != nil {
		t.Fatal(err)
	}

	digest, _ := GetSHA256Digest(f)
	if err := f.Close(); err != nil {
		t.Fatal(err)
	}

	if err := createLink(f.Name(), filepath.Join(modelDir, "blobs", fmt.Sprintf("sha256-%s", strings.TrimPrefix(digest, "sha256:")))); err != nil {
		t.Fatal(err)
	}

	return f.Name(), digest
}

type responseRecorder struct {
	*httptest.ResponseRecorder
	http.CloseNotifier
}

func NewRecorder() *responseRecorder {
	return &responseRecorder{
		ResponseRecorder: httptest.NewRecorder(),
	}
}

func (t *responseRecorder) CloseNotify() <-chan bool {
	return make(chan bool)
}

func createRequest(t *testing.T, fn func(*gin.Context), body any) *httptest.ResponseRecorder {
	t.Helper()
	// if OLLAMA_MODELS is not set, set it to the temp directory
	t.Setenv("OLLAMA_MODELS", cmp.Or(os.Getenv("OLLAMA_MODELS"), t.TempDir()))

	w := NewRecorder()
	c, _ := gin.CreateTestContext(w)

	var b bytes.Buffer
	if err := json.NewEncoder(&b).Encode(body); err != nil {
		t.Fatal(err)
	}

	c.Request = &http.Request{
		Body: io.NopCloser(&b),
	}

	fn(c)
	return w.ResponseRecorder
}

func TestCreateHandlerRejectsInvalidInfoTypes(t *testing.T) {
	gin.SetMode(gin.TestMode)

	testCases := []struct {
		name string
		info map[string]any
		err  string
	}{
		{
			name: "string_field",
			info: map[string]any{
				"model_family": float64(1),
			},
			err: "model_family",
		},
		{
			name: "fractional_integer_field",
			info: map[string]any{
				"context_length": 1.5,
			},
			err: "context_length",
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			var s Server
			_, digest := createBinFile(t, nil, nil)
			w := createRequest(t, s.CreateHandler, api.CreateRequest{
				Model: "test-create-invalid-info",
				Files: map[string]string{
					"test.gguf": digest,
				},
				Info:   tt.info,
				Stream: &stream,
			})

			if w.Code != http.StatusBadRequest {
				t.Fatalf("expected status code 400, got %d", w.Code)
			}

			var resp map[string]string
			if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
				t.Fatal(err)
			}
			if got := resp["error"]; !strings.Contains(got, tt.err) {
				t.Fatalf("expected %s error, got %q", tt.err, got)
			}
		})
	}
}

func readCreatedModelConfig(t *testing.T, name string) model.ConfigV2 {
	t.Helper()

	mf, err := manifest.ParseNamedManifest(model.ParseName(name))
	if err != nil {
		t.Fatalf("parse manifest: %v", err)
	}
	if mf.Config.Digest == "" {
		t.Fatalf("unexpected empty config digest for manifest")
	}

	configPath, err := manifest.BlobsPath(mf.Config.Digest)
	if err != nil {
		t.Fatalf("config blob path: %v", err)
	}

	cfgFile, err := os.Open(configPath)
	if err != nil {
		t.Fatalf("open config blob: %v", err)
	}
	defer cfgFile.Close()

	var cfg model.ConfigV2
	if err := json.NewDecoder(cfgFile).Decode(&cfg); err != nil {
		t.Fatalf("decode config: %v", err)
	}

	return cfg
}

func TestCreateModelPreservesEmbeddedCompatibilityGGUFWithoutQuantization(t *testing.T) {
	t.Setenv("OLLAMA_MODELS", t.TempDir())
	oldRun := runLlamaQuantize
	runLlamaQuantize = func(in, out *os.File, orig *ggml.GGML, fileType ggml.FileType, typeName string, progressFn func(uint64)) error {
		t.Fatal("llama-quantize should not run for GGUFs with embedded compatibility tensors")
		return nil
	}
	t.Cleanup(func() {
		runLlamaQuantize = oldRun
	})

	_, digest := createBinFile(t, map[string]any{
		"general.architecture": "gemma4",
		"general.file_type":    uint32(ggml.FileTypeF32),
	}, []*ggml.Tensor{
		{
			Name:     "v.patch_embd.weight",
			Kind:     uint32(ggml.TensorTypeF32),
			Shape:    []uint64{1, 1},
			WriterTo: bytes.NewReader(make([]byte, 4)),
		},
	})
	baseLayers, err := ggufLayers(digest, "test.gguf", func(api.ProgressResponse) {})
	if err != nil {
		t.Fatal(err)
	}

	name := model.ParseName("test-create-preserve-gguf:latest")
	config := &model.ConfigV2{
		OS:           "linux",
		Architecture: "amd64",
		RootFS:       model.RootFS{Type: "layers"},
	}
	req := api.CreateRequest{Model: name.String()}
	if err := createModel(req, name, baseLayers, config, func(api.ProgressResponse) {}); err != nil {
		t.Fatal(err)
	}

	mf, err := manifest.ParseNamedManifest(name)
	if err != nil {
		t.Fatal(err)
	}
	var found bool
	for _, layer := range mf.Layers {
		if layer.MediaType == "application/vnd.ollama.image.model" {
			found = true
		}
	}
	if !found {
		t.Fatal("created manifest is missing a model layer")
	}
	if mf.Layers[0].Digest != digest {
		t.Fatalf("model layer digest = %q, want original digest %q", mf.Layers[0].Digest, digest)
	}
}

func TestCreateModelValidatesTextOnlyFileGGUFWithoutQuantization(t *testing.T) {
	t.Setenv("OLLAMA_MODELS", t.TempDir())
	var gotTypeName string
	oldRun := runLlamaQuantize
	runLlamaQuantize = func(in, out *os.File, orig *ggml.GGML, fileType ggml.FileType, typeName string, progressFn func(uint64)) error {
		gotTypeName = typeName
		return copyLlamaQuantizeInput(in, out, orig, fileType, typeName, progressFn)
	}
	t.Cleanup(func() {
		runLlamaQuantize = oldRun
	})

	_, digest := createBinFile(t, map[string]any{
		"general.architecture": "llama",
		"general.file_type":    uint32(ggml.FileTypeF32),
	}, []*ggml.Tensor{
		{
			Name:     "blk.0.attn_q.weight",
			Kind:     uint32(ggml.TensorTypeF32),
			Shape:    []uint64{1, 1},
			WriterTo: bytes.NewReader(make([]byte, 4)),
		},
	})
	baseLayers, err := ggufLayers(digest, "test.gguf", func(api.ProgressResponse) {})
	if err != nil {
		t.Fatal(err)
	}

	name := model.ParseName("test-create-validate-gguf:latest")
	config := &model.ConfigV2{
		OS:           "linux",
		Architecture: "amd64",
		RootFS:       model.RootFS{Type: "layers"},
	}
	req := api.CreateRequest{Model: name.String()}
	if err := createModel(req, name, baseLayers, config, func(api.ProgressResponse) {}); err != nil {
		t.Fatal(err)
	}

	if gotTypeName != "COPY" {
		t.Fatalf("llama-quantize type = %q, want COPY", gotTypeName)
	}
}

func TestCreateModelValidatesSplitGGUFWithOriginalShardNames(t *testing.T) {
	t.Setenv("OLLAMA_MODELS", t.TempDir())
	var gotInput string
	oldRun := runLlamaQuantize
	runLlamaQuantize = func(in, out *os.File, orig *ggml.GGML, fileType ggml.FileType, typeName string, progressFn func(uint64)) error {
		gotInput = in.Name()
		if typeName != "COPY" {
			t.Fatalf("llama-quantize type = %q, want COPY", typeName)
		}
		if got := filepath.Base(in.Name()); got != "model-00001-of-00002.gguf" {
			t.Fatalf("llama-quantize input = %q, want first split shard name", got)
		}
		if _, err := os.Stat(filepath.Join(filepath.Dir(in.Name()), "model-00002-of-00002.gguf")); err != nil {
			t.Fatalf("missing linked second split shard: %v", err)
		}
		return ggml.WriteGGUF(out, ggml.KV{
			"general.architecture": "llama",
			"general.file_type":    fileType,
		}, []*ggml.Tensor{
			{
				Name:     "blk.0.attn_q.weight",
				Kind:     uint32(ggml.TensorTypeF32),
				Shape:    []uint64{1, 1},
				WriterTo: bytes.NewReader(make([]byte, 4)),
			},
		})
	}
	t.Cleanup(func() {
		runLlamaQuantize = oldRun
	})

	_, firstDigest := createBinFile(t, ggml.KV{
		"general.architecture":      "llama",
		"general.file_type":         uint32(ggml.FileTypeF32),
		"llama.split.no":            uint32(0),
		"llama.split.count":         uint32(2),
		"llama.split.tensors.count": int32(2),
	}, []*ggml.Tensor{
		{
			Name:     "blk.0.attn_q.weight",
			Kind:     uint32(ggml.TensorTypeF32),
			Shape:    []uint64{1, 1},
			WriterTo: bytes.NewReader(make([]byte, 4)),
		},
	})
	_, secondDigest := createBinFile(t, ggml.KV{
		"general.architecture":      "llama",
		"general.file_type":         uint32(ggml.FileTypeF32),
		"llama.split.no":            uint32(1),
		"llama.split.count":         uint32(2),
		"llama.split.tensors.count": int32(2),
	}, []*ggml.Tensor{
		{
			Name:     "blk.1.attn_q.weight",
			Kind:     uint32(ggml.TensorTypeF32),
			Shape:    []uint64{1, 1},
			WriterTo: bytes.NewReader(make([]byte, 4)),
		},
	})

	baseLayers, err := convertModelFromFiles(map[string]string{
		"model-00001-of-00002.gguf": firstDigest,
		"model-00002-of-00002.gguf": secondDigest,
	}, nil, false, func(api.ProgressResponse) {})
	if err != nil {
		t.Fatal(err)
	}

	name := model.ParseName("test-create-split-gguf:latest")
	config := &model.ConfigV2{
		OS:           "linux",
		Architecture: "amd64",
		RootFS:       model.RootFS{Type: "layers"},
	}
	req := api.CreateRequest{Model: name.String()}
	if err := createModel(req, name, baseLayers, config, func(api.ProgressResponse) {}); err != nil {
		t.Fatal(err)
	}
	if gotInput == "" {
		t.Fatal("llama-quantize was not invoked")
	}

	mf, err := manifest.ParseNamedManifest(name)
	if err != nil {
		t.Fatal(err)
	}
	var modelLayers int
	for _, layer := range mf.Layers {
		if layer.MediaType == "application/vnd.ollama.image.model" {
			modelLayers++
		}
	}
	if modelLayers != 1 {
		t.Fatalf("model layer count = %d, want 1", modelLayers)
	}
}

func TestBaseLayerTensorsReadsAllSplitGGUFShards(t *testing.T) {
	t.Setenv("OLLAMA_MODELS", t.TempDir())
	firstData := []byte{1, 2, 3, 4}
	secondData := []byte{5, 6, 7, 8}

	_, firstDigest := createBinFile(t, ggml.KV{
		"general.architecture":       "qwen35",
		"general.file_type":          uint32(ggml.FileTypeF32),
		"qwen35.split.no":            uint32(0),
		"qwen35.split.count":         uint32(2),
		"qwen35.split.tensors.count": int32(2),
	}, []*ggml.Tensor{
		{
			Name:     "blk.0.attn_q.weight",
			Kind:     uint32(ggml.TensorTypeF32),
			Shape:    []uint64{1},
			WriterTo: bytes.NewReader(firstData),
		},
	})
	_, secondDigest := createBinFile(t, ggml.KV{
		"general.architecture":       "qwen35",
		"general.file_type":          uint32(ggml.FileTypeF32),
		"qwen35.split.no":            uint32(1),
		"qwen35.split.count":         uint32(2),
		"qwen35.split.tensors.count": int32(2),
	}, []*ggml.Tensor{
		{
			Name:     "blk.1.attn_q.weight",
			Kind:     uint32(ggml.TensorTypeF32),
			Shape:    []uint64{1},
			WriterTo: bytes.NewReader(secondData),
		},
	})

	baseLayers, err := convertModelFromFiles(map[string]string{
		"model-00001-of-00002.gguf": firstDigest,
		"model-00002-of-00002.gguf": secondDigest,
	}, nil, false, func(api.ProgressResponse) {})
	if err != nil {
		t.Fatal(err)
	}
	if got, want := len(baseLayers), 1; got != want {
		t.Fatalf("base layers = %d, want %d", got, want)
	}

	tensors, cleanup, err := baseLayerTensors(baseLayers[0])
	if err != nil {
		t.Fatal(err)
	}
	defer cleanup()

	byName := map[string][]byte{}
	for _, tensor := range tensors {
		var b bytes.Buffer
		if _, err := tensor.WriteTo(&b); err != nil {
			t.Fatal(err)
		}
		byName[tensor.Name] = b.Bytes()
	}

	if got := byName["blk.0.attn_q.weight"]; !bytes.Equal(got, firstData) {
		t.Fatalf("first shard tensor data = %v, want %v", got, firstData)
	}
	if got := byName["blk.1.attn_q.weight"]; !bytes.Equal(got, secondData) {
		t.Fatalf("second shard tensor data = %v, want %v", got, secondData)
	}
}

func TestCreateModelAddsDefaultLlavaProjectorType(t *testing.T) {
	t.Setenv("OLLAMA_MODELS", t.TempDir())

	_, digest := createBinFile(t, map[string]any{
		"general.architecture":    "clip",
		"clip.has_vision_encoder": true,
		"clip.vision.block_count": uint32(1),
	}, []*ggml.Tensor{
		{
			Name:     "mm.0.weight",
			Kind:     uint32(ggml.TensorTypeF32),
			Shape:    []uint64{1, 1},
			WriterTo: bytes.NewReader(make([]byte, 4)),
		},
	})
	baseLayers, err := ggufLayers(digest, "test.gguf", func(api.ProgressResponse) {})
	if err != nil {
		t.Fatal(err)
	}

	name := model.ParseName("test-create-llava-projector:latest")
	config := &model.ConfigV2{
		OS:           "linux",
		Architecture: "amd64",
		RootFS:       model.RootFS{Type: "layers"},
	}
	req := api.CreateRequest{Model: name.String()}
	if err := createModel(req, name, baseLayers, config, func(api.ProgressResponse) {}); err != nil {
		t.Fatal(err)
	}

	mf, err := manifest.ParseNamedManifest(name)
	if err != nil {
		t.Fatal(err)
	}
	var projectorLayer manifest.Layer
	for _, layer := range mf.Layers {
		if layer.MediaType == "application/vnd.ollama.image.projector" {
			projectorLayer = layer
			break
		}
	}
	if projectorLayer.Digest == "" {
		t.Fatal("created manifest is missing a projector layer")
	}
	if projectorLayer.Digest == digest {
		t.Fatal("projector layer was not rewritten")
	}

	blobPath, err := manifest.BlobsPath(projectorLayer.Digest)
	if err != nil {
		t.Fatal(err)
	}
	blob, err := os.Open(blobPath)
	if err != nil {
		t.Fatal(err)
	}
	defer blob.Close()

	projector, err := ggml.Decode(blob, -1)
	if err != nil {
		t.Fatal(err)
	}
	if got := projector.KV().String("projector_type"); got != "mlp" {
		t.Fatalf("clip.projector_type = %q, want mlp", got)
	}
}

func TestGGUFLayersClassifiesMMProjAsProjector(t *testing.T) {
	t.Setenv("OLLAMA_MODELS", t.TempDir())

	_, digest := createBinFile(t, map[string]any{
		"general.architecture":            "clip",
		"general.type":                    "mmproj",
		"clip.has_vision_encoder":         true,
		"clip.vision.block_count":         uint32(0),
		"clip.vision.projector_type":      "gemma4uv",
		"clip.has_audio_encoder":          true,
		"clip.audio.block_count":          uint32(0),
		"clip.audio.projector_type":       "gemma4ua",
		"clip.vision.projection_dim":      uint32(3840),
		"clip.vision.embedding_length":    uint32(3840),
		"clip.audio.projection_dim":       uint32(3840),
		"clip.audio.embedding_length":     uint32(3840),
		"clip.vision.feed_forward_length": uint32(0),
		"clip.audio.feed_forward_length":  uint32(0),
	}, []*ggml.Tensor{
		{
			Name:     "mm.input_projection.weight",
			Kind:     uint32(ggml.TensorTypeF32),
			Shape:    []uint64{1, 1},
			WriterTo: bytes.NewReader(make([]byte, 4)),
		},
	})
	layers, err := ggufLayers(digest, "mmproj-gemma-4-12B-it-bf16.gguf", func(api.ProgressResponse) {})
	if err != nil {
		t.Fatal(err)
	}
	if len(layers) != 1 {
		t.Fatalf("layers = %d, want 1", len(layers))
	}
	if got := layers[0].MediaType; got != "application/vnd.ollama.image.projector" {
		t.Fatalf("media type = %q, want projector", got)
	}
}

func TestCreateModelQuantizeRestoresEmbeddedCompatibilityTensors(t *testing.T) {
	t.Setenv("OLLAMA_MODELS", t.TempDir())
	oldRun := runLlamaQuantize
	runLlamaQuantize = func(in, out *os.File, orig *ggml.GGML, fileType ggml.FileType, typeName string, progressFn func(uint64)) error {
		kv := ggml.KV{
			"general.architecture":             "qwen2vl",
			"general.file_type":                fileType,
			"qwen25vl.attention.head_count":    uint32(1),
			"qwen25vl.attention.head_count_kv": uint32(1),
		}
		return ggml.WriteGGUF(out, kv, []*ggml.Tensor{
			{
				Name:     "blk.0.attn_q.weight",
				Kind:     uint32(ggml.TensorTypeQ4_K),
				Shape:    []uint64{256, 1},
				WriterTo: bytes.NewReader(make([]byte, int(ggml.TensorTypeQ4_K.RowSize(256)))),
			},
		})
	}
	t.Cleanup(func() {
		runLlamaQuantize = oldRun
	})

	_, digest := createBinFile(t, map[string]any{
		"general.architecture":             "qwen25vl",
		"general.file_type":                uint32(ggml.FileTypeF16),
		"qwen25vl.attention.head_count":    uint32(8),
		"qwen25vl.attention.head_count_kv": uint32(2),
	}, []*ggml.Tensor{
		{
			Name:     "blk.0.attn_q.weight",
			Kind:     uint32(ggml.TensorTypeF16),
			Shape:    []uint64{256, 1},
			WriterTo: bytes.NewReader(make([]byte, 512)),
		},
		{
			Name:     "v.patch_embd.weight",
			Kind:     uint32(ggml.TensorTypeF16),
			Shape:    []uint64{1, 1},
			WriterTo: bytes.NewReader(make([]byte, 2)),
		},
	})
	baseLayers, err := ggufLayers(digest, "test.gguf", func(api.ProgressResponse) {})
	if err != nil {
		t.Fatal(err)
	}

	name := model.ParseName("test-create-quantize-embedded-gguf:latest")
	config := &model.ConfigV2{
		OS:           "linux",
		Architecture: "amd64",
		RootFS:       model.RootFS{Type: "layers"},
	}
	req := api.CreateRequest{Model: name.String(), Quantize: "Q4_K_M"}
	if err := createModel(req, name, baseLayers, config, func(api.ProgressResponse) {}); err != nil {
		t.Fatal(err)
	}

	mf, err := manifest.ParseNamedManifest(name)
	if err != nil {
		t.Fatal(err)
	}

	var modelLayer manifest.Layer
	for _, layer := range mf.Layers {
		if layer.MediaType == "application/vnd.ollama.image.model" {
			modelLayer = layer
			break
		}
	}
	if modelLayer.Digest == "" {
		t.Fatal("created manifest is missing a model layer")
	}

	blobPath, err := manifest.BlobsPath(modelLayer.Digest)
	if err != nil {
		t.Fatal(err)
	}
	blob, err := os.Open(blobPath)
	if err != nil {
		t.Fatal(err)
	}
	defer blob.Close()

	f, err := ggml.Decode(blob, -1)
	if err != nil {
		t.Fatal(err)
	}
	byName := map[string]*ggml.Tensor{}
	for _, tensor := range f.Tensors().Items() {
		byName[tensor.Name] = tensor
	}
	if got := f.KV().FileType(); got != ggml.FileTypeQ4_K_M {
		t.Fatalf("file type = %s, want Q4_K_M", got)
	}
	if got := f.KV().Architecture(); got != "qwen25vl" {
		t.Fatalf("architecture = %q, want qwen25vl", got)
	}
	if got, _ := f.KV().Value("qwen25vl.attention.head_count").(uint32); got != 8 {
		t.Fatalf("attention head count = %d, want 8", got)
	}
	if got := ggml.TensorType(byName["blk.0.attn_q.weight"].Kind); got != ggml.TensorTypeQ4_K {
		t.Fatalf("text tensor type = %s, want Q4_K", got)
	}
	if got := ggml.TensorType(byName["v.patch_embd.weight"].Kind); got != ggml.TensorTypeF16 {
		t.Fatalf("embedded tensor type = %s, want F16", got)
	}
}

func TestCreateModelRejectsFileGGUFWhenValidationFails(t *testing.T) {
	t.Setenv("OLLAMA_MODELS", t.TempDir())
	oldRun := runLlamaQuantize
	runLlamaQuantize = func(*os.File, *os.File, *ggml.GGML, ggml.FileType, string, func(uint64)) error {
		return fmt.Errorf("load failed")
	}
	t.Cleanup(func() {
		runLlamaQuantize = oldRun
	})

	_, digest := createBinFile(t, map[string]any{
		"general.architecture": "llama",
		"general.file_type":    uint32(ggml.FileTypeF32),
	}, []*ggml.Tensor{
		{
			Name:     "blk.0.attn_q.weight",
			Kind:     uint32(ggml.TensorTypeF32),
			Shape:    []uint64{1, 1},
			WriterTo: bytes.NewReader(make([]byte, 4)),
		},
	})
	baseLayers, err := ggufLayers(digest, "test.gguf", func(api.ProgressResponse) {})
	if err != nil {
		t.Fatal(err)
	}

	name := model.ParseName("test-create-reject-gguf:latest")
	config := &model.ConfigV2{
		OS:           "linux",
		Architecture: "amd64",
		RootFS:       model.RootFS{Type: "layers"},
	}
	req := api.CreateRequest{Model: name.String()}
	err = createModel(req, name, baseLayers, config, func(api.ProgressResponse) {})
	if err == nil {
		t.Fatal("expected create to fail")
	}
	if !strings.Contains(err.Error(), "failed to validate GGUF with llama-quantize without compatibility patches") {
		t.Fatalf("unexpected error: %v", err)
	}
}

func checkFileExists(t *testing.T, p string, expect []string) {
	t.Helper()

	actual, err := filepath.Glob(p)
	if err != nil {
		t.Fatal(err)
	}

	if diff := gocmp.Diff(expect, actual, gocmpopts.SortSlices(strings.Compare), gocmpopts.EquateEmpty()); diff != "" {
		t.Errorf("file exists mismatch (-want +got):\n%s", diff)
	}
}

func TestCreateFromBin(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)

	var s Server

	_, digest := createBinFile(t, nil, nil)

	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:   "test",
		Files:  map[string]string{"test.gguf": digest},
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		fmt.Println(w)
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
	})

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-6bcdb8859d417753645538d7bbfbd7ca91a3f0c191aef5379c53c05e86b669dd"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
	})

	t.Run("empty file digest", func(t *testing.T) {
		w := createRequest(t, s.CreateHandler, api.CreateRequest{
			Name:   "my-gguf-model",
			Files:  map[string]string{"0.gguf": ""},
			Stream: &stream,
		})

		if w.Code != http.StatusBadRequest {
			t.Fatalf("expected status 400, got %d", w.Code)
		}
		if !strings.Contains(w.Body.String(), "invalid digest format") {
			t.Errorf("expected invalid digest format error, got:\n%s", w.Body.String())
		}
	})

	t.Run("empty adapter digest", func(t *testing.T) {
		w := createRequest(t, s.CreateHandler, api.CreateRequest{
			Name:     "my-gguf-model",
			Files:    map[string]string{"0.gguf": digest},
			Adapters: map[string]string{"adapter.gguf": ""},
			Stream:   &stream,
		})

		if w.Code != http.StatusBadRequest {
			t.Fatalf("expected status 400, got %d", w.Code)
		}
		if !strings.Contains(w.Body.String(), "invalid digest format") {
			t.Errorf("expected invalid digest format error, got:\n%s", w.Body.String())
		}
	})
}

func TestCreateFromModel(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	_, digest := createBinFile(t, nil, nil)

	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:   "test",
		Files:  map[string]string{"test.gguf": digest},
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
	})

	w = createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:   "test2",
		From:   "test",
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test2", "latest"),
	})

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-6bcdb8859d417753645538d7bbfbd7ca91a3f0c191aef5379c53c05e86b669dd"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
	})
}

func TestCreateFromModelInheritsRendererParser(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	const (
		renderer = "custom-renderer"
		parser   = "custom-parser"
	)

	_, digest := createBinFile(t, nil, nil)

	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:     "base",
		Files:    map[string]string{"base.gguf": digest},
		Renderer: renderer,
		Parser:   parser,
		Stream:   &stream,
	})
	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	w = createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:   "child",
		From:   "base",
		Stream: &stream,
	})
	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	mf, err := manifest.ParseNamedManifest(model.ParseName("child"))
	if err != nil {
		t.Fatalf("parse manifest: %v", err)
	}
	if mf.Config.Digest == "" {
		t.Fatalf("unexpected empty config digest for child manifest")
	}

	configPath, err := manifest.BlobsPath(mf.Config.Digest)
	if err != nil {
		t.Fatalf("config blob path: %v", err)
	}

	cfgFile, err := os.Open(configPath)
	if err != nil {
		t.Fatalf("open config blob: %v", err)
	}
	defer cfgFile.Close()

	var cfg model.ConfigV2
	if err := json.NewDecoder(cfgFile).Decode(&cfg); err != nil {
		t.Fatalf("decode config: %v", err)
	}

	if cfg.Renderer != renderer {
		t.Fatalf("expected renderer %q, got %q", renderer, cfg.Renderer)
	}
	if cfg.Parser != parser {
		t.Fatalf("expected parser %q, got %q", parser, cfg.Parser)
	}
}

func TestCreateRemovesLayers(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	_, digest := createBinFile(t, nil, nil)
	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:     "test",
		Files:    map[string]string{"test.gguf": digest},
		Template: "{{ .Prompt }}",
		Stream:   &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
	})

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
		filepath.Join(p, "blobs", "sha256-b507b9c2f6ca642bffcd06665ea7c91f235fd32daeefdf875a0f938db05fb315"),
		filepath.Join(p, "blobs", "sha256-f6e7e4b28e0b1d0c635f2d465bd248c5387c3e75b61a48c4374192b26d832a56"),
	})

	w = createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:     "test",
		Files:    map[string]string{"test.gguf": digest},
		Template: "{{ .System }} {{ .Prompt }}",
		Stream:   &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
	})

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-136bf7c76bac2ec09d6617885507d37829e04b41acc47687d45e512b544e893a"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
		filepath.Join(p, "blobs", "sha256-fe7ac77b725cda2ccad03f88a880ecdfd7a33192d6cae08fce2c0ee1455991ed"),
	})
}

func TestCreateUnsetsSystem(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	_, digest := createBinFile(t, nil, nil)
	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:   "test",
		Files:  map[string]string{"test.gguf": digest},
		System: "Say hi!",
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
	})

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-0a666d113e8e0a3d27e9c7bd136a0bdfb6241037db50729d81568451ebfdbde8"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
		filepath.Join(p, "blobs", "sha256-f29e82a8284dbdf5910b1555580ff60b04238b8da9d5e51159ada67a4d0d5851"),
	})

	w = createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:   "test",
		Files:  map[string]string{"test.gguf": digest},
		System: "",
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
	})

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-6bcdb8859d417753645538d7bbfbd7ca91a3f0c191aef5379c53c05e86b669dd"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
	})
}

func TestCreateMergeParameters(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	_, digest := createBinFile(t, nil, nil)
	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:  "test",
		Files: map[string]string{"test.gguf": digest},
		Parameters: map[string]any{
			"temperature": 1,
			"top_k":       10,
			"stop":        []string{"USER:", "ASSISTANT:"},
		},
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
	})

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-1d0ad71299d48c2fb7ae2b98e683643e771f8a5b72be34942af90d97a91c1e37"),
		filepath.Join(p, "blobs", "sha256-6d6e36c1f90fc7deefc33a7300aa21ad4b67c506e33ecdeddfafa98147e60bbf"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
	})

	// in order to merge parameters, the second model must be created FROM the first
	w = createRequest(t, s.CreateHandler, api.CreateRequest{
		Name: "test2",
		From: "test",
		Parameters: map[string]any{
			"temperature": 0.6,
			"top_p":       0.7,
		},
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test2", "latest"),
	})

	// Display contents of each blob in the directory
	blobDir := filepath.Join(p, "blobs")
	entries, err := os.ReadDir(blobDir)
	if err != nil {
		t.Fatalf("failed to read blobs directory: %v", err)
	}

	for _, entry := range entries {
		blobPath := filepath.Join(blobDir, entry.Name())
		content, err := os.ReadFile(blobPath)
		if err != nil {
			t.Fatalf("failed to read blob %s: %v", entry.Name(), err)
		}
		t.Logf("Contents of %s:\n%s", entry.Name(), string(content))
	}

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-1d0ad71299d48c2fb7ae2b98e683643e771f8a5b72be34942af90d97a91c1e37"),
		filepath.Join(p, "blobs", "sha256-6d6e36c1f90fc7deefc33a7300aa21ad4b67c506e33ecdeddfafa98147e60bbf"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
		filepath.Join(p, "blobs", "sha256-bbdce269dabe013033632238b4b2d1e02fac2f97787c5e895f4da84e09cccd5d"),
		filepath.Join(p, "blobs", "sha256-e29a7b3c47287a2489c895d21fe413c20f859a85d20e749492f52a838e36e1ba"),
	})

	actual, err := os.ReadFile(filepath.Join(p, "blobs", "sha256-e29a7b3c47287a2489c895d21fe413c20f859a85d20e749492f52a838e36e1ba"))
	if err != nil {
		t.Fatal(err)
	}

	expect, err := json.Marshal(map[string]any{"temperature": 0.6, "top_k": 10, "top_p": 0.7, "stop": []string{"USER:", "ASSISTANT:"}})
	if err != nil {
		t.Fatal(err)
	}

	if !bytes.Equal(bytes.TrimSpace(expect), bytes.TrimSpace(actual)) {
		t.Errorf("expected %s, actual %s", string(expect), string(actual))
	}

	// slices are replaced
	w = createRequest(t, s.CreateHandler, api.CreateRequest{
		Name: "test2",
		From: "test",
		Parameters: map[string]any{
			"temperature": 0.6,
			"top_p":       0.7,
			"stop":        []string{"<|endoftext|>"},
		},
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test2", "latest"),
	})

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-12f58bb75cb3042d69a7e013ab87fb3c3c7088f50ddc62f0c77bd332f0d44d35"),
		filepath.Join(p, "blobs", "sha256-1d0ad71299d48c2fb7ae2b98e683643e771f8a5b72be34942af90d97a91c1e37"),
		filepath.Join(p, "blobs", "sha256-6d6e36c1f90fc7deefc33a7300aa21ad4b67c506e33ecdeddfafa98147e60bbf"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
		filepath.Join(p, "blobs", "sha256-9443591d14be23c1e33d101934d76ad03bdb0715fe0879e8b0d1819e7bb063dd"),
	})

	actual, err = os.ReadFile(filepath.Join(p, "blobs", "sha256-12f58bb75cb3042d69a7e013ab87fb3c3c7088f50ddc62f0c77bd332f0d44d35"))
	if err != nil {
		t.Fatal(err)
	}

	expect, err = json.Marshal(map[string]any{"temperature": 0.6, "top_k": 10, "top_p": 0.7, "stop": []string{"<|endoftext|>"}})
	if err != nil {
		t.Fatal(err)
	}

	if !bytes.Equal(bytes.TrimSpace(expect), bytes.TrimSpace(actual)) {
		t.Errorf("expected %s, actual %s", string(expect), string(actual))
	}
}

func TestCreateReplacesMessages(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	_, digest := createBinFile(t, nil, nil)
	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:  "test",
		Files: map[string]string{"test.gguf": digest},
		Messages: []api.Message{
			{
				Role:    "assistant",
				Content: "What is my purpose?",
			},
			{
				Role:    "user",
				Content: "You run tests.",
			},
			{
				Role:    "assistant",
				Content: "Oh, my god.",
			},
		},
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
	})

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-298baeaf6928a60cf666d88d64a1ba606feb43a2865687c39e40652e407bffc4"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
		filepath.Join(p, "blobs", "sha256-c84aee28f2af350596f674de51d2a802ea782653ef2930a21d48bd43d5cd5317"),
	})

	w = createRequest(t, s.CreateHandler, api.CreateRequest{
		Name: "test2",
		From: "test",
		Messages: []api.Message{
			{
				Role:    "assistant",
				Content: "You're a test, Harry.",
			},
			{
				Role:    "user",
				Content: "I-I'm a what?",
			},
			{
				Role:    "assistant",
				Content: "A test. And a thumping good one at that, I'd wager.",
			},
		},
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test2", "latest"),
	})

	// Old layers will not have been pruned
	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-09cfac3e6a637e25cb41aa85c24c110dc17ba89634de7df141b564dd2da4168b"),
		filepath.Join(p, "blobs", "sha256-298baeaf6928a60cf666d88d64a1ba606feb43a2865687c39e40652e407bffc4"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
		filepath.Join(p, "blobs", "sha256-a60ecc9da299ec7ede453f99236e5577fd125e143689b646d9f0ddc9971bf4db"),
		filepath.Join(p, "blobs", "sha256-c84aee28f2af350596f674de51d2a802ea782653ef2930a21d48bd43d5cd5317"),
	})

	type message struct {
		Role    string `json:"role"`
		Content string `json:"content"`
	}

	f, err := os.Open(filepath.Join(p, "blobs", "sha256-a60ecc9da299ec7ede453f99236e5577fd125e143689b646d9f0ddc9971bf4db"))
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()

	var actual []message
	if err := json.NewDecoder(f).Decode(&actual); err != nil {
		t.Fatal(err)
	}

	expect := []message{
		{Role: "assistant", Content: "You're a test, Harry."},
		{Role: "user", Content: "I-I'm a what?"},
		{Role: "assistant", Content: "A test. And a thumping good one at that, I'd wager."},
	}

	if !slices.Equal(actual, expect) {
		t.Errorf("expected %s, actual %s", expect, actual)
	}
}

func TestCreateTemplateSystem(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	_, digest := createBinFile(t, nil, nil)
	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:     "test",
		Files:    map[string]string{"test.gguf": digest},
		Template: "{{ .System }} {{ .Prompt }}",
		System:   "Say bye!",
		Stream:   &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
	})

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-0a04d979734167da3b80811a1874d734697f366a689f3912589b99d2e86e7ad1"),
		filepath.Join(p, "blobs", "sha256-4c5f51faac758fecaff8db42f0b7382891a4d0c0bb885f7b86be88c814a7cc86"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
		filepath.Join(p, "blobs", "sha256-fe7ac77b725cda2ccad03f88a880ecdfd7a33192d6cae08fce2c0ee1455991ed"),
	})

	template, err := os.ReadFile(filepath.Join(p, "blobs", "sha256-fe7ac77b725cda2ccad03f88a880ecdfd7a33192d6cae08fce2c0ee1455991ed"))
	if err != nil {
		t.Fatal(err)
	}

	if string(template) != "{{ .System }} {{ .Prompt }}" {
		t.Errorf("expected \"{{ .System }} {{ .Prompt }}\", actual %s", template)
	}

	system, err := os.ReadFile(filepath.Join(p, "blobs", "sha256-4c5f51faac758fecaff8db42f0b7382891a4d0c0bb885f7b86be88c814a7cc86"))
	if err != nil {
		t.Fatal(err)
	}

	if string(system) != "Say bye!" {
		t.Errorf("expected \"Say bye!\", actual %s", system)
	}

	t.Run("incomplete template", func(t *testing.T) {
		_, digest := createBinFile(t, nil, nil)
		w := createRequest(t, s.CreateHandler, api.CreateRequest{
			Name:     "test",
			Files:    map[string]string{"test.gguf": digest},
			Template: "{{ .Prompt",
			Stream:   &stream,
		})

		if w.Code != http.StatusBadRequest {
			t.Fatalf("expected status code 400, actual %d", w.Code)
		}
	})

	t.Run("template with unclosed if", func(t *testing.T) {
		_, digest := createBinFile(t, nil, nil)
		w := createRequest(t, s.CreateHandler, api.CreateRequest{
			Name:     "test",
			Files:    map[string]string{"test.gguf": digest},
			Template: "{{ if .Prompt }}",
			Stream:   &stream,
		})

		if w.Code != http.StatusBadRequest {
			t.Fatalf("expected status code 400, actual %d", w.Code)
		}
	})

	t.Run("template with undefined function", func(t *testing.T) {
		_, digest := createBinFile(t, nil, nil)
		w := createRequest(t, s.CreateHandler, api.CreateRequest{
			Name:     "test",
			Files:    map[string]string{"test.gguf": digest},
			Template: "{{ Prompt }}",
			Stream:   &stream,
		})

		if w.Code != http.StatusBadRequest {
			t.Fatalf("expected status code 400, actual %d", w.Code)
		}
	})
}

func TestCreateAndShowRemoteModel(t *testing.T) {
	gin.SetMode(gin.TestMode)

	var s Server

	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Model:      "test",
		From:       "bob",
		RemoteHost: "https://ollama.com",
		Info: map[string]any{
			"capabilities":       []string{"completion", "tools", "thinking"},
			"model_family":       "gptoss",
			"context_length":     131072,
			"embedding_length":   2880,
			"quantization_level": "MXFP4",
			"parameter_size":     "20.9B",
		},
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("exected status code 200, actual %d", w.Code)
	}

	w = createRequest(t, s.ShowHandler, api.ShowRequest{Model: "test"})
	if w.Code != http.StatusOK {
		t.Fatalf("exected status code 200, actual %d", w.Code)
	}

	var resp api.ShowResponse
	if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
		t.Fatal(err)
	}

	expectedDetails := api.ModelDetails{
		ParentModel:       "",
		Format:            "",
		Family:            "gptoss",
		Families:          []string{"gptoss"},
		ParameterSize:     "20.9B",
		QuantizationLevel: "MXFP4",
	}

	if !reflect.DeepEqual(resp.Details, expectedDetails) {
		t.Errorf("model details: expected %#v, actual %#v", expectedDetails, resp.Details)
	}

	expectedCaps := []model.Capability{
		model.Capability("completion"),
		model.Capability("tools"),
		model.Capability("thinking"),
	}

	if !slices.Equal(resp.Capabilities, expectedCaps) {
		t.Errorf("capabilities: expected %#v, actual %#v", expectedCaps, resp.Capabilities)
	}

	v, ok := resp.ModelInfo["gptoss.context_length"]
	ctxlen := v.(float64)
	if !ok || int(ctxlen) != 131072 {
		t.Errorf("context len: expected %d, actual %d", 131072, int(ctxlen))
	}

	v, ok = resp.ModelInfo["gptoss.embedding_length"]
	embedlen := v.(float64)
	if !ok || int(embedlen) != 2880 {
		t.Errorf("embed len: expected %d, actual %d", 2880, int(embedlen))
	}

	fmt.Printf("resp = %#v\n", resp)
}

func TestCreateRemoteModelRejectsDraftFiles(t *testing.T) {
	gin.SetMode(gin.TestMode)

	var s Server
	_, digest := createBinFile(t, nil, nil)

	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Model:      "test-remote-draft",
		From:       "bob",
		RemoteHost: "https://ollama.com",
		DraftFiles: map[string]string{"draft.gguf": digest},
		Stream:     &stream,
	})

	if w.Code != http.StatusBadRequest {
		t.Fatalf("expected status code 400, got %d", w.Code)
	}

	var resp map[string]string
	if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
		t.Fatal(err)
	}
	if resp["error"] != errRemoteDraftUnsupported.Error() {
		t.Fatalf("expected error %q, got %q", errRemoteDraftUnsupported, resp["error"])
	}
}

func TestCreateFromCloudSourceSuffix(t *testing.T) {
	gin.SetMode(gin.TestMode)

	var s Server

	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Model: "test-cloud-from-suffix",
		From:  "gpt-oss:20b:cloud",
		Info: map[string]any{
			"capabilities": []string{"completion"},
		},
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, got %d", w.Code)
	}

	w = createRequest(t, s.ShowHandler, api.ShowRequest{Model: "test-cloud-from-suffix"})
	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, got %d", w.Code)
	}

	var resp api.ShowResponse
	if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
		t.Fatal(err)
	}

	if resp.RemoteHost != "https://ollama.com:443" {
		t.Fatalf("expected remote host https://ollama.com:443, got %q", resp.RemoteHost)
	}

	if resp.RemoteModel != "gpt-oss:20b" {
		t.Fatalf("expected remote model gpt-oss:20b, got %q", resp.RemoteModel)
	}
}

func TestCreateLicenses(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	_, digest := createBinFile(t, nil, nil)
	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:    "test",
		Files:   map[string]string{"test.gguf": digest},
		License: []string{"MIT", "Apache-2.0"},
		Stream:  &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	checkFileExists(t, filepath.Join(p, "manifests", "*", "*", "*", "*"), []string{
		filepath.Join(p, "manifests", "registry.ollama.ai", "library", "test", "latest"),
	})

	checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
		filepath.Join(p, "blobs", "sha256-2af71558e438db0b73a20beab92dc278a94e1bbe974c00c1a33e3ab62d53a608"),
		filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
		filepath.Join(p, "blobs", "sha256-a762f214df0d96c9a7b82f96da98d99ceb2776c88e3ea7ffa09d1e5835516ec6"),
		filepath.Join(p, "blobs", "sha256-e5dcffe836b6ec8a58e492419b550e65fb8cbdc308503979e5dacb33ac7ea3b7"),
	})

	mit, err := os.ReadFile(filepath.Join(p, "blobs", "sha256-e5dcffe836b6ec8a58e492419b550e65fb8cbdc308503979e5dacb33ac7ea3b7"))
	if err != nil {
		t.Fatal(err)
	}

	if string(mit) != "MIT" {
		t.Errorf("expected MIT, actual %s", mit)
	}

	apache, err := os.ReadFile(filepath.Join(p, "blobs", "sha256-2af71558e438db0b73a20beab92dc278a94e1bbe974c00c1a33e3ab62d53a608"))
	if err != nil {
		t.Fatal(err)
	}

	if string(apache) != "Apache-2.0" {
		t.Errorf("expected Apache-2.0, actual %s", apache)
	}
}

func TestCreateDetectTemplate(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	t.Run("matched", func(t *testing.T) {
		_, digest := createBinFile(t, ggml.KV{
			"tokenizer.chat_template": "{{ bos_token }}{% for message in messages %}{{'<|' + message['role'] + '|>' + '\n' + message['content'] + '<|end|>\n' }}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|>\n' }}{% else %}{{ eos_token }}{% endif %}",
		}, nil)
		w := createRequest(t, s.CreateHandler, api.CreateRequest{
			Name:   "test",
			Files:  map[string]string{"test.gguf": digest},
			Stream: &stream,
		})

		if w.Code != http.StatusOK {
			t.Fatalf("expected status code 200, actual %d", w.Code)
		}

		checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
			filepath.Join(p, "blobs", "sha256-0d79f567714c62c048378f2107fb332dabee0135d080c302d884317da9433cc5"),
			filepath.Join(p, "blobs", "sha256-3322a0c650c758b7386ff55629d27d07c07b6c3d3515e259dc3e5598c41e9f4e"),
			filepath.Join(p, "blobs", "sha256-35360843d0c84fb1506952a131bbef13cd2bb4a541251f22535170c05b56e672"),
			filepath.Join(p, "blobs", "sha256-a56c12acca8068cb6c335e237da6643e8a802a92959a63ad5bd17828e3b5e9b0"),
		})
	})

	t.Run("unmatched", func(t *testing.T) {
		_, digest := createBinFile(t, nil, nil)
		w := createRequest(t, s.CreateHandler, api.CreateRequest{
			Name:   "test",
			Files:  map[string]string{"test.gguf": digest},
			Stream: &stream,
		})

		if w.Code != http.StatusOK {
			t.Fatalf("expected status code 200, actual %d", w.Code)
		}

		checkFileExists(t, filepath.Join(p, "blobs", "*"), []string{
			filepath.Join(p, "blobs", "sha256-6bcdb8859d417753645538d7bbfbd7ca91a3f0c191aef5379c53c05e86b669dd"),
			filepath.Join(p, "blobs", "sha256-89a2116c3a82d6a97f59f748d86ed4417214353fd178ee54df418fde32495fad"),
		})
	})
}

func TestCreateGemma4KeepsDynamicRendererAlias(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	_, digest := createBinFile(t, ggml.KV{
		"general.architecture":    "gemma4",
		"general.parameter_count": uint64(25_200_000_000),
	}, nil)

	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:   "test",
		Files:  map[string]string{"test.gguf": digest},
		Stream: &stream,
	})
	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	mf, err := manifest.ParseNamedManifest(model.ParseName("test"))
	if err != nil {
		t.Fatalf("parse manifest: %v", err)
	}
	if mf.Config.Digest == "" {
		t.Fatalf("unexpected empty config digest for manifest")
	}

	configPath, err := manifest.BlobsPath(mf.Config.Digest)
	if err != nil {
		t.Fatalf("config blob path: %v", err)
	}

	cfgFile, err := os.Open(configPath)
	if err != nil {
		t.Fatalf("open config blob: %v", err)
	}
	defer cfgFile.Close()

	var cfg model.ConfigV2
	if err := json.NewDecoder(cfgFile).Decode(&cfg); err != nil {
		t.Fatalf("decode config: %v", err)
	}

	if cfg.Renderer != gemma4RendererLegacy {
		t.Fatalf("expected renderer %q, got %q", gemma4RendererLegacy, cfg.Renderer)
	}
	if cfg.Parser != "gemma4" {
		t.Fatalf("expected parser %q, got %q", "gemma4", cfg.Parser)
	}
}

func TestCreateLagunaDetectsRendererParser(t *testing.T) {
	gin.SetMode(gin.TestMode)

	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	_, digest := createBinFile(t, ggml.KV{
		"general.architecture":    "laguna",
		"general.parameter_count": uint64(33_400_000_000),
	}, nil)

	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Name:   "test",
		Files:  map[string]string{"test.gguf": digest},
		Stream: &stream,
	})
	if w.Code != http.StatusOK {
		t.Fatalf("expected status code 200, actual %d", w.Code)
	}

	mf, err := manifest.ParseNamedManifest(model.ParseName("test"))
	if err != nil {
		t.Fatalf("parse manifest: %v", err)
	}
	if mf.Config.Digest == "" {
		t.Fatalf("unexpected empty config digest for manifest")
	}

	configPath, err := manifest.BlobsPath(mf.Config.Digest)
	if err != nil {
		t.Fatalf("config blob path: %v", err)
	}

	cfgFile, err := os.Open(configPath)
	if err != nil {
		t.Fatalf("open config blob: %v", err)
	}
	defer cfgFile.Close()

	var cfg model.ConfigV2
	if err := json.NewDecoder(cfgFile).Decode(&cfg); err != nil {
		t.Fatalf("decode config: %v", err)
	}

	if cfg.Renderer != "laguna" {
		t.Fatalf("expected renderer %q, got %q", "laguna", cfg.Renderer)
	}
	if cfg.Parser != "laguna" {
		t.Fatalf("expected parser %q, got %q", "laguna", cfg.Parser)
	}
}

func TestCreateNemotronHDefaultsRendererParser(t *testing.T) {
	gin.SetMode(gin.TestMode)

	for _, arch := range []string{"nemotron_h", "nemotron_h_moe", "nemotron_h_omni"} {
		t.Run(arch, func(t *testing.T) {
			p := t.TempDir()
			t.Setenv("OLLAMA_MODELS", p)
			var s Server

			_, digest := createBinFile(t, ggml.KV{
				"general.architecture": arch,
			}, nil)

			name := strings.ReplaceAll(arch, "_", "-")
			w := createRequest(t, s.CreateHandler, api.CreateRequest{
				Name:   name,
				Files:  map[string]string{"test.gguf": digest},
				Stream: &stream,
			})
			if w.Code != http.StatusOK {
				t.Fatalf("expected status code 200, actual %d", w.Code)
			}

			cfg := readCreatedModelConfig(t, name)
			if cfg.Renderer != "nemotron-3-nano" {
				t.Fatalf("expected renderer %q, got %q", "nemotron-3-nano", cfg.Renderer)
			}
			if cfg.Parser != "nemotron-3-nano" {
				t.Fatalf("expected parser %q, got %q", "nemotron-3-nano", cfg.Parser)
			}
		})
	}
}

func TestCreateNemotronHDefaultsKeepExplicitRendererParser(t *testing.T) {
	gin.SetMode(gin.TestMode)

	for _, arch := range []string{"nemotron_h", "nemotron_h_moe", "nemotron_h_omni"} {
		t.Run(arch, func(t *testing.T) {
			p := t.TempDir()
			t.Setenv("OLLAMA_MODELS", p)
			var s Server

			_, digest := createBinFile(t, ggml.KV{
				"general.architecture": arch,
			}, nil)

			const (
				renderer = "custom-renderer"
				parser   = "custom-parser"
			)

			name := strings.ReplaceAll(arch, "_", "-") + "-custom"
			w := createRequest(t, s.CreateHandler, api.CreateRequest{
				Name:     name,
				Files:    map[string]string{"test.gguf": digest},
				Renderer: renderer,
				Parser:   parser,
				Stream:   &stream,
			})
			if w.Code != http.StatusOK {
				t.Fatalf("expected status code 200, actual %d", w.Code)
			}

			cfg := readCreatedModelConfig(t, name)
			if cfg.Renderer != renderer {
				t.Fatalf("expected renderer %q, got %q", renderer, cfg.Renderer)
			}
			if cfg.Parser != parser {
				t.Fatalf("expected parser %q, got %q", parser, cfg.Parser)
			}
		})
	}
}

func TestDetectModelTypeFromFiles(t *testing.T) {
	t.Run("gguf file", func(t *testing.T) {
		_, digest := createBinFile(t, nil, nil)
		files := map[string]string{
			"model.gguf": digest,
		}

		modelType := detectModelTypeFromFiles(files)
		if modelType != "gguf" {
			t.Fatalf("expected model type 'gguf', got %q", modelType)
		}
	})

	t.Run("gguf file w/o extension", func(t *testing.T) {
		_, digest := createBinFile(t, nil, nil)
		files := map[string]string{
			fmt.Sprintf("%x", digest): digest,
		}

		modelType := detectModelTypeFromFiles(files)
		if modelType != "gguf" {
			t.Fatalf("expected model type 'gguf', got %q", modelType)
		}
	})

	t.Run("safetensors file", func(t *testing.T) {
		files := map[string]string{
			"model.safetensors": "sha256:abc123",
		}

		modelType := detectModelTypeFromFiles(files)
		if modelType != "safetensors" {
			t.Fatalf("expected model type 'safetensors', got %q", modelType)
		}
	})

	t.Run("unsupported file type", func(t *testing.T) {
		p := t.TempDir()
		t.Setenv("OLLAMA_MODELS", p)

		data := []byte("12345678")
		digest := fmt.Sprintf("sha256:%x", sha256.Sum256(data))
		if err := os.MkdirAll(filepath.Join(p, "blobs"), 0o755); err != nil {
			t.Fatal(err)
		}

		f, err := os.Create(filepath.Join(p, "blobs", fmt.Sprintf("sha256-%s", strings.TrimPrefix(digest, "sha256:"))))
		if err != nil {
			t.Fatal(err)
		}
		defer f.Close()

		if _, err := f.Write(data); err != nil {
			t.Fatal(err)
		}

		files := map[string]string{
			"model.bin": digest,
		}

		modelType := detectModelTypeFromFiles(files)
		if modelType != "" {
			t.Fatalf("expected empty model type for unsupported file, got %q", modelType)
		}
	})

	t.Run("file with less than 4 bytes", func(t *testing.T) {
		p := t.TempDir()
		t.Setenv("OLLAMA_MODELS", p)

		data := []byte("123")
		digest := fmt.Sprintf("sha256:%x", sha256.Sum256(data))
		if err := os.MkdirAll(filepath.Join(p, "blobs"), 0o755); err != nil {
			t.Fatal(err)
		}

		f, err := os.Create(filepath.Join(p, "blobs", fmt.Sprintf("sha256-%s", strings.TrimPrefix(digest, "sha256:"))))
		if err != nil {
			t.Fatal(err)
		}
		defer f.Close()

		if _, err := f.Write(data); err != nil {
			t.Fatal(err)
		}

		files := map[string]string{
			"noext": digest,
		}

		modelType := detectModelTypeFromFiles(files)
		if modelType != "" {
			t.Fatalf("expected empty model type for small file, got %q", modelType)
		}
	})
}

// createTestBlob creates a blob in the blobs directory and returns its digest.
func createTestBlob(t *testing.T, data []byte) string {
	t.Helper()
	digest := fmt.Sprintf("sha256:%x", sha256.Sum256(data))
	blobPath, err := manifest.BlobsPath(digest)
	if err != nil {
		t.Fatal(err)
	}
	if err := os.MkdirAll(filepath.Dir(blobPath), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(blobPath, data, 0o644); err != nil {
		t.Fatal(err)
	}
	return digest
}

// createSafetensorsTestModel creates a minimal safetensors model manifest for testing.
func createSafetensorsTestModel(t *testing.T, modelName string, config model.ConfigV2, extraLayers []manifest.Layer) {
	t.Helper()

	// Create a fake tensor blob
	tensorData := []byte("fake-tensor-data-for-testing")
	tensorDigest := createTestBlob(t, tensorData)

	layers := []manifest.Layer{
		{
			MediaType: manifest.MediaTypeImageTensor,
			Digest:    tensorDigest,
			Size:      int64(len(tensorData)),
			Name:      "model.embed_tokens.weight",
		},
	}
	layers = append(layers, extraLayers...)

	configLayer, err := createConfigLayer(layers, config)
	if err != nil {
		t.Fatalf("failed to create config layer: %v", err)
	}

	name := model.ParseName(modelName)
	if err := manifest.WriteManifest(name, *configLayer, layers); err != nil {
		t.Fatalf("failed to write manifest: %v", err)
	}
}

func TestCreateFromSafetensorsModel_PreservesConfig(t *testing.T) {
	gin.SetMode(gin.TestMode)
	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	// Create a source safetensors model with specific config fields
	createSafetensorsTestModel(t, "source-model", model.ConfigV2{
		ModelFormat:  "safetensors",
		Capabilities: []string{"completion"},
		Requires:     "0.14.0",
		Renderer:     "gemma3",
		Parser:       "gemma3",
	}, nil)

	// Create a derived model FROM the source
	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Model:  "derived-model",
		From:   "source-model",
		System: "You are a pirate.",
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
	}

	// Read the derived model's config
	derivedName := model.ParseName("derived-model")
	mf, err := manifest.ParseNamedManifest(derivedName)
	if err != nil {
		t.Fatalf("failed to parse derived manifest: %v", err)
	}

	configBlobPath, err := manifest.BlobsPath(mf.Config.Digest)
	if err != nil {
		t.Fatalf("failed to get config blob path: %v", err)
	}

	configBlob, err := os.ReadFile(configBlobPath)
	if err != nil {
		t.Fatalf("failed to read config blob: %v", err)
	}

	var cfg model.ConfigV2
	if err := json.Unmarshal(configBlob, &cfg); err != nil {
		t.Fatalf("failed to unmarshal config: %v", err)
	}

	// Verify safetensors-specific config fields are preserved
	if cfg.ModelFormat != "safetensors" {
		t.Errorf("ModelFormat = %q, want %q", cfg.ModelFormat, "safetensors")
	}

	if !slices.Contains(cfg.Capabilities, "completion") {
		t.Errorf("Capabilities = %v, want to contain %q", cfg.Capabilities, "completion")
	}

	if cfg.Requires != "0.14.0" {
		t.Errorf("Requires = %q, want %q", cfg.Requires, "0.14.0")
	}

	if cfg.Renderer != "gemma3" {
		t.Errorf("Renderer = %q, want %q", cfg.Renderer, "gemma3")
	}

	if cfg.Parser != "gemma3" {
		t.Errorf("Parser = %q, want %q", cfg.Parser, "gemma3")
	}

	// Verify system prompt was added
	var hasSystem bool
	for _, l := range mf.Layers {
		if l.MediaType == "application/vnd.ollama.image.system" {
			hasSystem = true
			break
		}
	}
	if !hasSystem {
		t.Error("expected system prompt layer in derived model")
	}

	// Verify tensor layers were copied with names preserved
	var tensorNames []string
	for _, l := range mf.Layers {
		if l.MediaType == manifest.MediaTypeImageTensor {
			tensorNames = append(tensorNames, l.Name)
		}
	}
	if len(tensorNames) == 0 {
		t.Error("expected tensor layers in derived model")
	}
	for _, name := range tensorNames {
		if name == "" {
			t.Error("tensor layer has empty name — names must be preserved from source")
		}
	}
}

func TestCreateFromSafetensorsModel_OverrideSystem(t *testing.T) {
	gin.SetMode(gin.TestMode)
	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	// Create source with a system prompt
	createSafetensorsTestModel(t, "source-with-system", model.ConfigV2{
		ModelFormat:  "safetensors",
		Capabilities: []string{"completion"},
	}, nil)

	// First create with system prompt
	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Model:  "source-with-system",
		From:   "source-with-system",
		System: "Original system prompt",
		Stream: &stream,
	})
	if w.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
	}

	// Now create a derived model with a different system prompt
	w = createRequest(t, s.CreateHandler, api.CreateRequest{
		Model:  "derived-new-system",
		From:   "source-with-system",
		System: "New system prompt",
		Stream: &stream,
	})
	if w.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
	}

	// Verify ModelFormat is preserved even after override
	derivedName := model.ParseName("derived-new-system")
	mf, err := manifest.ParseNamedManifest(derivedName)
	if err != nil {
		t.Fatalf("failed to parse derived manifest: %v", err)
	}

	configBlobPath, _ := manifest.BlobsPath(mf.Config.Digest)
	configBlob, _ := os.ReadFile(configBlobPath)

	var cfg model.ConfigV2
	json.Unmarshal(configBlob, &cfg)

	if cfg.ModelFormat != "safetensors" {
		t.Errorf("ModelFormat = %q, want %q", cfg.ModelFormat, "safetensors")
	}
}

func TestCreateFromSafetensorsModel_PreservesLayerNames(t *testing.T) {
	gin.SetMode(gin.TestMode)
	p := t.TempDir()
	t.Setenv("OLLAMA_MODELS", p)
	var s Server

	// Create JSON config blobs to include as layers
	configJSON := []byte(`{"architectures": ["LlamaForCausalLM"], "model_type": "llama"}`)
	configDigest := createTestBlob(t, configJSON)
	tokenizerJSON := []byte(`{"version": "1.0"}`)
	tokenizerDigest := createTestBlob(t, tokenizerJSON)

	extraLayers := []manifest.Layer{
		{
			MediaType: "application/vnd.ollama.image.json",
			Digest:    configDigest,
			Size:      int64(len(configJSON)),
			Name:      "config.json",
		},
		{
			MediaType: "application/vnd.ollama.image.json",
			Digest:    tokenizerDigest,
			Size:      int64(len(tokenizerJSON)),
			Name:      "tokenizer.json",
		},
	}

	createSafetensorsTestModel(t, "source-named-layers", model.ConfigV2{
		ModelFormat:  "safetensors",
		Capabilities: []string{"completion"},
	}, extraLayers)

	// Create derived model
	w := createRequest(t, s.CreateHandler, api.CreateRequest{
		Model:  "derived-named-layers",
		From:   "source-named-layers",
		Stream: &stream,
	})

	if w.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
	}

	derivedName := model.ParseName("derived-named-layers")
	mf, err := manifest.ParseNamedManifest(derivedName)
	if err != nil {
		t.Fatalf("failed to parse derived manifest: %v", err)
	}

	// Check tensor layer names are preserved
	for _, l := range mf.Layers {
		if l.MediaType == manifest.MediaTypeImageTensor && l.Name == "" {
			t.Error("tensor layer has empty name — names must be preserved from source")
		}
	}

	// Check JSON layer names are preserved
	jsonNames := make(map[string]bool)
	for _, l := range mf.Layers {
		if l.MediaType == "application/vnd.ollama.image.json" && l.Name != "" {
			jsonNames[l.Name] = true
		}
	}

	if !jsonNames["config.json"] {
		t.Error("config.json layer name not preserved in derived model")
	}
	if !jsonNames["tokenizer.json"] {
		t.Error("tokenizer.json layer name not preserved in derived model")
	}
}
