package artifacts

import (
	"encoding/json"
	"strings"
	"testing"
)

func TestRunManifestOmitsEmptyOptionalFields(t *testing.T) {
	gotJSON, err := json.Marshal(RunManifest{})
	if err != nil {
		t.Fatalf("json.Marshal() error = %v", err)
	}

	if string(gotJSON) != "{}" {
		t.Fatalf("json.Marshal(RunManifest{}) = %s, want {}", gotJSON)
	}
}

func TestRunManifestChunkPlanIsAdditiveAndOmitsPlanContent(t *testing.T) {
	manifest := RunManifest{ChunkPlan: &ChunkPlanManifest{
		Mode: "auto", Action: "reused", SourceDigest: "sha256:source", PlanDigest: "sha256:plan",
		PlanSchemaVersion: "notarius.chunk-plan.v2", RequestedModule: "chunk/current",
		ProducerInputModule: "input/original", ProducerModule: "chunk/original",
	}}
	encoded, err := json.Marshal(manifest)
	if err != nil {
		t.Fatal(err)
	}
	text := string(encoded)
	for _, want := range []string{`"chunk_plan"`, `"action":"reused"`, `"requested_module":"chunk/current"`, `"producer_module":"chunk/original"`} {
		if !strings.Contains(text, want) {
			t.Fatalf("manifest JSON %s does not contain %s", text, want)
		}
	}
	for _, forbidden := range []string{`"plan"`, `"units"`, `"annotations"`} {
		if strings.Contains(text, forbidden) {
			t.Fatalf("manifest JSON contains forbidden field %s: %s", forbidden, text)
		}
	}

	var legacy RunManifest
	if err := json.Unmarshal([]byte(`{"pipeline_id":"legacy"}`), &legacy); err != nil {
		t.Fatal(err)
	}
	if legacy.PipelineID != "legacy" || legacy.ChunkPlan != nil {
		t.Fatalf("legacy manifest = %#v", legacy)
	}
}

func TestRunManifestIncludesPipelineAndArtifactLaneFields(t *testing.T) {
	manifest := RunManifest{
		PipelineID:     "pipeline-1",
		PipelineDigest: "sha256:abc123",
		LLMProfiles: []LLMProfileManifest{
			{
				ID:              "default",
				Provider:        "promptkit",
				Model:           "model-a",
				BackendID:       "openrouter",
				ReasoningEffort: "high",
			},
		},
		ArtifactLanes: []ArtifactLaneManifest{
			{
				ID:         "events",
				Extractor:  "event-extractor",
				Merger:     "appendorder",
				Normalizer: "noop",
				Metadata: map[string]any{
					"extractor": map[string]any{"prompt_id": "test.prompt"},
				},
			},
		},
		ValidatorChains: []ValidatorChainManifest{
			{
				Stage:     "extract",
				LaneID:    "events",
				ModuleKey: "event-extractor",
				Validators: []ValidatorManifest{
					{Key: "grounded", ExecutionClass: "deterministic"},
				},
			},
		},
	}

	gotJSON, err := json.Marshal(manifest)
	if err != nil {
		t.Fatalf("json.Marshal() error = %v", err)
	}

	var got map[string]any
	if err := json.Unmarshal(gotJSON, &got); err != nil {
		t.Fatalf("json.Unmarshal() error = %v", err)
	}

	assertHasKeys(t, got, "pipeline_id", "pipeline_digest", "artifact_lanes", "validator_chains", "llm_profiles")

	profiles, ok := got["llm_profiles"].([]any)
	if !ok {
		t.Fatalf("llm_profiles = %#v, want array", got["llm_profiles"])
	}
	if len(profiles) != 1 {
		t.Fatalf("len(llm_profiles) = %d, want 1", len(profiles))
	}
	profile, ok := profiles[0].(map[string]any)
	if !ok {
		t.Fatalf("llm_profiles[0] = %#v, want object", profiles[0])
	}
	assertHasKeys(t, profile, "id", "provider", "model", "backend_id", "reasoning_effort")
	if profile["provider"] != "promptkit" {
		t.Fatalf("llm_profiles[0].provider = %#v, want promptkit", profile["provider"])
	}
	if profile["backend_id"] != "openrouter" || profile["reasoning_effort"] != "high" {
		t.Fatalf("llm_profiles[0] = %#v, want backend and reasoning provenance", profile)
	}

	lanes, ok := got["artifact_lanes"].([]any)
	if !ok {
		t.Fatalf("artifact_lanes = %#v, want array", got["artifact_lanes"])
	}
	if len(lanes) != 1 {
		t.Fatalf("len(artifact_lanes) = %d, want 1", len(lanes))
	}
	lane, ok := lanes[0].(map[string]any)
	if !ok {
		t.Fatalf("artifact_lanes[0] = %#v, want object", lanes[0])
	}
	assertHasKeys(t, lane, "id", "extractor", "merger", "normalizer", "metadata")

	chains, ok := got["validator_chains"].([]any)
	if !ok {
		t.Fatalf("validator_chains = %#v, want array", got["validator_chains"])
	}
	if len(chains) != 1 {
		t.Fatalf("len(validator_chains) = %d, want 1", len(chains))
	}
	chain, ok := chains[0].(map[string]any)
	if !ok {
		t.Fatalf("validator_chains[0] = %#v, want object", chains[0])
	}
	assertHasKeys(t, chain, "stage", "lane_id", "module_key", "validators")
}

func TestRunManifestIncludesReferenceProvenance(t *testing.T) {
	manifest := RunManifest{
		References: []ReferenceProvenance{
			{
				Stage:         "extract",
				LaneID:        "events",
				SlotName:      "roster",
				OriginType:    "file",
				OriginURI:     "file:///tmp/roster.txt",
				Digest:        "sha256:reference",
				MediaType:     "text/plain; charset=utf-8",
				SizeBytes:     12,
				BindingSource: "config",
			},
		},
	}

	gotJSON, err := json.Marshal(manifest)
	if err != nil {
		t.Fatalf("json.Marshal() error = %v", err)
	}

	var got RunManifest
	if err := json.Unmarshal(gotJSON, &got); err != nil {
		t.Fatalf("json.Unmarshal() error = %v", err)
	}
	if len(got.References) != 1 {
		t.Fatalf("len(References) = %d, want 1", len(got.References))
	}
	reference := got.References[0]
	if reference.Stage != "extract" || reference.LaneID != "events" || reference.SlotName != "roster" || reference.OriginType != "file" || reference.OriginURI != "file:///tmp/roster.txt" {
		t.Fatalf("reference provenance = %#v, want lane-scoped origin details", reference)
	}
	if reference.Digest != "sha256:reference" || reference.MediaType != "text/plain; charset=utf-8" || reference.SizeBytes != 12 || reference.BindingSource != "config" {
		t.Fatalf("reference provenance = %#v, want digest/media/size/source details", reference)
	}
}

func TestRunManifestIncludesTopLevelModuleMetadata(t *testing.T) {
	manifest := RunManifest{
		ModuleMetadata: map[string]map[string]any{
			"chunker": {
				"prompt_id":            "dnd.scenes",
				"prompt_version":       "v1",
				"prompt_sha256":        "sha256:abc123",
				"response_schema_key":  "dnd_scenes_llm",
				"response_schema_name": "notarius_dnd_scenes_llm_v1",
			},
		},
	}

	gotJSON, err := json.Marshal(manifest)
	if err != nil {
		t.Fatalf("json.Marshal() error = %v", err)
	}

	var got map[string]any
	if err := json.Unmarshal(gotJSON, &got); err != nil {
		t.Fatalf("json.Unmarshal() error = %v", err)
	}

	moduleMetadata, ok := got["module_metadata"].(map[string]any)
	if !ok {
		t.Fatalf("module_metadata = %#v, want object", got["module_metadata"])
	}
	assertHasKeys(t, moduleMetadata, "chunker")

	chunkerMetadata, ok := moduleMetadata["chunker"].(map[string]any)
	if !ok {
		t.Fatalf("module_metadata.chunker = %#v, want object", moduleMetadata["chunker"])
	}
	assertHasKeys(t, chunkerMetadata, "prompt_id", "prompt_version", "prompt_sha256", "response_schema_key", "response_schema_name")
}

func assertHasKeys(t *testing.T, values map[string]any, keys ...string) {
	t.Helper()

	for _, key := range keys {
		if _, ok := values[key]; !ok {
			t.Fatalf("missing key %q in %#v", key, values)
		}
	}
}
