package output

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

	"github.com/salvacybersec/keyhunter/pkg/engine"
)

// decodeSARIF runs the formatter and unmarshals the output into a sarifDoc
// for structural assertions.
func decodeSARIF(t *testing.T, findings []engine.Finding, opts Options) sarifDoc {
	t.Helper()
	var buf bytes.Buffer
	if err := (SARIFFormatter{}).Format(findings, &buf, opts); err != nil {
		t.Fatalf("Format returned error: %v", err)
	}
	var doc sarifDoc
	if err := json.Unmarshal(buf.Bytes(), &doc); err != nil {
		t.Fatalf("failed to unmarshal SARIF output: %v\npayload: %s", err, buf.String())
	}
	return doc
}

func TestSARIF_Empty(t *testing.T) {
	doc := decodeSARIF(t, nil, Options{})
	if doc.Version != "2.1.0" {
		t.Errorf("version: got %q want 2.1.0", doc.Version)
	}
	if doc.Schema != "https://json.schemastore.org/sarif-2.1.0.json" {
		t.Errorf("schema: got %q", doc.Schema)
	}
	if len(doc.Runs) != 1 {
		t.Fatalf("runs: got %d want 1", len(doc.Runs))
	}
	if len(doc.Runs[0].Results) != 0 {
		t.Errorf("results: got %d want 0", len(doc.Runs[0].Results))
	}
	if len(doc.Runs[0].Tool.Driver.Rules) != 0 {
		t.Errorf("rules: got %d want 0", len(doc.Runs[0].Tool.Driver.Rules))
	}
}

func TestSARIF_DedupRules(t *testing.T) {
	findings := []engine.Finding{
		{ProviderName: "openai", KeyMasked: "sk-aaaa...bbbb", Confidence: "high", Source: "a.go", LineNumber: 10},
		{ProviderName: "openai", KeyMasked: "sk-cccc...dddd", Confidence: "high", Source: "b.go", LineNumber: 20},
		{ProviderName: "anthropic", KeyMasked: "sk-ant-x...y", Confidence: "medium", Source: "c.go", LineNumber: 5},
	}
	doc := decodeSARIF(t, findings, Options{})
	rules := doc.Runs[0].Tool.Driver.Rules
	if len(rules) != 2 {
		t.Fatalf("rules: got %d want 2", len(rules))
	}
	seen := map[string]bool{}
	for _, r := range rules {
		seen[r.ID] = true
		if r.Name != r.ID {
			t.Errorf("rule name %q != id %q", r.Name, r.ID)
		}
		if !strings.Contains(r.ShortDescription.Text, "Leaked") {
			t.Errorf("shortDescription missing 'Leaked': %q", r.ShortDescription.Text)
		}
	}
	if !seen["openai"] || !seen["anthropic"] {
		t.Errorf("expected openai and anthropic rules, got %+v", seen)
	}
	if got := len(doc.Runs[0].Results); got != 3 {
		t.Errorf("results: got %d want 3", got)
	}
}

func TestSARIF_LevelMapping(t *testing.T) {
	cases := map[string]string{
		"high":    "error",
		"medium":  "warning",
		"low":     "note",
		"unknown": "warning",
	}
	for conf, wantLevel := range cases {
		findings := []engine.Finding{{ProviderName: "p", KeyMasked: "k", Confidence: conf, Source: "f", LineNumber: 1}}
		doc := decodeSARIF(t, findings, Options{})
		if got := doc.Runs[0].Results[0].Level; got != wantLevel {
			t.Errorf("confidence %q: got level %q want %q", conf, got, wantLevel)
		}
	}
}

func TestSARIF_LineFloor(t *testing.T) {
	findings := []engine.Finding{
		{ProviderName: "p", KeyMasked: "k", Confidence: "high", Source: "f", LineNumber: 0},
		{ProviderName: "p", KeyMasked: "k", Confidence: "high", Source: "f", LineNumber: -5},
		{ProviderName: "p", KeyMasked: "k", Confidence: "high", Source: "f", LineNumber: 42},
	}
	doc := decodeSARIF(t, findings, Options{})
	results := doc.Runs[0].Results
	if results[0].Locations[0].PhysicalLocation.Region.StartLine != 1 {
		t.Errorf("zero line should floor to 1, got %d", results[0].Locations[0].PhysicalLocation.Region.StartLine)
	}
	if results[1].Locations[0].PhysicalLocation.Region.StartLine != 1 {
		t.Errorf("negative line should floor to 1, got %d", results[1].Locations[0].PhysicalLocation.Region.StartLine)
	}
	if results[2].Locations[0].PhysicalLocation.Region.StartLine != 42 {
		t.Errorf("line 42 should pass through, got %d", results[2].Locations[0].PhysicalLocation.Region.StartLine)
	}
	if results[0].Locations[0].PhysicalLocation.ArtifactLocation.URI != "f" {
		t.Errorf("artifactLocation.uri not preserved")
	}
}

func TestSARIF_Masking(t *testing.T) {
	findings := []engine.Finding{{
		ProviderName: "openai",
		KeyValue:     "sk-SECRETVALUE1234",
		KeyMasked:    "sk-SECRE...1234",
		Confidence:   "high",
		Source:       "a.go",
		LineNumber:   1,
	}}

	// Default: masked.
	doc := decodeSARIF(t, findings, Options{})
	msg := doc.Runs[0].Results[0].Message.Text
	if !strings.Contains(msg, "sk-SECRE...1234") {
		t.Errorf("masked message should contain KeyMasked, got %q", msg)
	}
	if strings.Contains(msg, "SECRETVALUE1234") {
		t.Errorf("masked message leaked full key: %q", msg)
	}

	// Unmasked.
	doc = decodeSARIF(t, findings, Options{Unmask: true})
	msg = doc.Runs[0].Results[0].Message.Text
	if !strings.Contains(msg, "sk-SECRETVALUE1234") {
		t.Errorf("unmasked message should contain KeyValue, got %q", msg)
	}
}

func TestSARIF_ToolVersionFallback(t *testing.T) {
	doc := decodeSARIF(t, nil, Options{})
	d := doc.Runs[0].Tool.Driver
	if d.Name != "keyhunter" {
		t.Errorf("default tool name: got %q want keyhunter", d.Name)
	}
	if d.Version != "dev" {
		t.Errorf("default tool version: got %q want dev", d.Version)
	}

	doc = decodeSARIF(t, nil, Options{ToolName: "custom", ToolVersion: "1.2.3"})
	d = doc.Runs[0].Tool.Driver
	if d.Name != "custom" || d.Version != "1.2.3" {
		t.Errorf("explicit name/version not honored: %+v", d)
	}
}

func TestSARIF_RegisteredInRegistry(t *testing.T) {
	f, err := Get("sarif")
	if err != nil {
		t.Fatalf("sarif formatter not registered: %v", err)
	}
	if _, ok := f.(SARIFFormatter); !ok {
		t.Errorf("registered formatter is not SARIFFormatter: %T", f)
	}
}
