package semanticreconcile

import (
	"context"
	"io/fs"
	"reflect"
	"strings"
	"testing"
	"time"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
	"gitea.maximumdirect.net/eric/promptkit"
)

func TestRegisterAssetsPreparesGenericPromptOffline(t *testing.T) {
	registry := llm.NewAssetRegistry()
	if err := RegisterAssets(registry); err != nil {
		t.Fatalf("RegisterAssets() error = %v, want nil", err)
	}
	options, err := registry.PromptKitOptions()
	if err != nil {
		t.Fatalf("PromptKitOptions() error = %v, want nil", err)
	}
	options = append(options, promptkit.WithProfiles(promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{
		ID: "semantic-reconciliation-test", Endpoint: "http://127.0.0.1:1/v1", Model: "offline-test-model",
	})))
	engine, err := promptkit.NewEngine(promptkit.Config{Timeout: time.Second}, options...)
	if err != nil {
		t.Fatalf("NewEngine() error = %v, want nil", err)
	}
	prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
		PromptID: PromptID, PromptVersion: PromptVersion, ProfileID: "semantic-reconciliation-test",
		Inputs: map[string]promptkit.ArtifactRef{
			"candidates": promptkit.Inline(`{"candidate_number_range":{"first":1,"last":2},"candidates":[{"candidate_number":1,"label":"Mira"},{"candidate_number":2,"label":"Captain Mira"}]}`),
			"transcript": promptkit.Inline(`{"windows":[{"units":[{"unit_id":7,"text":"Mira arrived."}]}]}`),
		},
	})
	if err != nil {
		t.Fatalf("Prepare() error = %v, want nil", err)
	}

	if prepared.PromptID != PromptID || prepared.PromptVersion != PromptVersion {
		t.Fatalf("prepared prompt identity = %q %q, want %q %q", prepared.PromptID, prepared.PromptVersion, PromptID, PromptVersion)
	}
	if prepared.SelectedProfileID != "semantic-reconciliation-test" {
		t.Fatalf("selected profile = %q, want explicit test profile", prepared.SelectedProfileID)
	}
	if contract := prepared.OutputContract; contract.SchemaPath != "semantic_reconciliation_llm.v1.json" || contract.RepairAttempts != 1 {
		t.Fatalf("output contract = %#v, want generic schema with one repair", contract)
	}
	if len(prepared.Messages) != 5 || prepared.Messages[0].Role != "system" {
		t.Fatalf("prepared messages = %#v, want five ordered messages beginning with system", prepared.Messages)
	}
	if cache := prepared.Messages[2].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
		t.Fatalf("semantic policy cache control = %#v, want ephemeral", cache)
	}
	for _, index := range []int{0, 1, 3, 4} {
		if prepared.Messages[index].CacheControl != nil {
			t.Fatalf("message %d cache control = %#v, want nil", index, prepared.Messages[index].CacheControl)
		}
	}
	protocol := prepared.Messages[1].Content
	for _, requirement := range []string{"candidate_number_range", "Return candidate numbers only", "do not copy candidate names", "start_unit_id", "not candidate numbers"} {
		if !strings.Contains(protocol, requirement) {
			t.Fatalf("protocol message = %q, want requirement %q", protocol, requirement)
		}
	}
	if !strings.Contains(prepared.Messages[3].Content, `"candidate_number":1`) || strings.Contains(prepared.Messages[3].Content, `"windows"`) {
		t.Fatalf("candidate message = %q, want only integer candidate material", prepared.Messages[3].Content)
	}
	if !strings.Contains(prepared.Messages[4].Content, `"windows"`) || strings.Contains(prepared.Messages[4].Content, `"candidate_number"`) {
		t.Fatalf("transcript message = %q, want only transcript windows", prepared.Messages[4].Content)
	}
}

func TestAssetHashesAreDeterministicAndComplete(t *testing.T) {
	firstPrompt, err := PromptHash()
	if err != nil {
		t.Fatal(err)
	}
	secondPrompt, err := PromptHash()
	if err != nil {
		t.Fatal(err)
	}
	schemaHash, err := SchemaHash()
	if err != nil {
		t.Fatal(err)
	}
	if firstPrompt == "" || firstPrompt != secondPrompt || schemaHash == "" || firstPrompt == schemaHash {
		t.Fatalf("asset hashes = prompt %q/%q schema %q, want stable distinct hashes", firstPrompt, secondPrompt, schemaHash)
	}
}

func TestSharedPromptFilesExposeOnlyReusableCoreAssets(t *testing.T) {
	first, err := SharedPromptFiles()
	if err != nil {
		t.Fatal(err)
	}
	second, err := SharedPromptFiles()
	if err != nil {
		t.Fatal(err)
	}
	wantNames := []string{"protocol.md", "candidates.md", "transcript-windows.md"}
	gotNames := make([]string, len(first))
	for index, file := range first {
		gotNames[index] = file.Name
		if content, err := fs.ReadFile(file.FS, file.Path); err != nil || len(content) == 0 {
			t.Fatalf("shared file %q = %q, %v; want readable content", file.Name, content, err)
		}
	}
	if !reflect.DeepEqual(gotNames, wantNames) {
		t.Fatalf("shared files = %#v, want narrow allowlist %#v", gotNames, wantNames)
	}
	first[0].Name = "changed.md"
	if second[0].Name != "protocol.md" {
		t.Fatalf("SharedPromptFiles() reused mutable descriptors: %#v", second)
	}
}
