// Package security — security_scan_test.go exercises the
// security-scan configuration files in v0.2 P03. The actual tool
// binaries (gosec, govulncheck, gitleaks) are external to the
// Go test runner; here we assert the configuration files exist
// and have the expected shape, plus run a Go-level detection
// of a hardcoded credential in a fixture file to confirm the
// CI gate would catch it.
//
// These tests run as part of `go test ./...` and require no
// external tools.
package security

import (
	"encoding/json"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

// TestGitleaksConfigExists verifies the .gitleaks.toml file is
// present and parseable. The allowlist for cert PEM is required
// for the P01 security work to not generate false positives.
func TestGitleaksConfigExists(t *testing.T) {
	root, err := findRepoRoot()
	if err != nil {
		t.Fatalf("findRepoRoot: %v", err)
	}
	path := filepath.Join(root, ".gitleaks.toml")
	if _, err := os.Stat(path); err != nil {
		t.Fatalf(".gitleaks.toml missing at %s: %v", path, err)
	}
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read .gitleaks.toml: %v", err)
	}
	s := string(body)
	for _, must := range []string{
		"orca-cert-pem",
		"BEGIN CERTIFICATE",
		"internal/security/testdata",
	} {
		if !strings.Contains(s, must) {
			t.Errorf(".gitleaks.toml missing required token: %q", must)
		}
	}
}

// TestGitleaksBaselineRoundTrip checks that the baseline file
// exists and has the expected JSON shape. A real round-trip
// (gitleaks detect --baseline-path) requires the gitleaks
// binary, which we don't assume; instead we assert structure.
func TestGitleaksBaselineRoundTrip(t *testing.T) {
	root, err := findRepoRoot()
	if err != nil {
		t.Fatalf("findRepoRoot: %v", err)
	}
	path := filepath.Join(root, ".gitleaks-baseline.json")
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read baseline: %v", err)
	}
	var entries []map[string]any
	if err := json.Unmarshal(body, &entries); err != nil {
		t.Fatalf("parse baseline: %v", err)
	}
	if len(entries) == 0 {
		t.Error("baseline empty: should suppress at least the v0.1 .env leak")
	}
	for i, e := range entries {
		if e["Op"] != "skip" {
			t.Errorf("entry %d: Op=%v, want skip", i, e["Op"])
		}
		if _, ok := e["Commit"]; !ok {
			t.Errorf("entry %d: missing Commit", i)
		}
		if _, ok := e["File"]; !ok {
			t.Errorf("entry %d: missing File", i)
		}
	}
}

// TestGolangciYmlShape verifies the .golangci.yml has the
// required linters enabled (REQ-040). We don't run golangci-lint
// here because it's an external binary; we just check that the
// linters we expect are listed.
func TestGolangciYmlShape(t *testing.T) {
	root, err := findRepoRoot()
	if err != nil {
		t.Fatalf("findRepoRoot: %v", err)
	}
	path := filepath.Join(root, ".golangci.yml")
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read .golangci.yml: %v", err)
	}
	s := string(body)
	for _, linter := range []string{"gosec", "govet", "ineffassign", "misspell"} {
		if !strings.Contains(s, "- "+linter) && !strings.Contains(s, linter+":") {
			t.Errorf(".golangci.yml: linter %q not enabled", linter)
		}
	}
}

// TestSecurityScanScriptShape checks that the wrapper script
// exists, is executable, and invokes all three tools.
func TestSecurityScanScriptShape(t *testing.T) {
	root, err := findRepoRoot()
	if err != nil {
		t.Fatalf("findRepoRoot: %v", err)
	}
	path := filepath.Join(root, "scripts", "security_scan.sh")
	info, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat: %v", err)
	}
	if info.Mode()&0o100 == 0 {
		t.Error("security_scan.sh is not executable (mode should include 0100)")
	}
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	s := string(body)
	for _, must := range []string{"gosec", "govulncheck", "gitleaks", "GOFLAGS=-mod=mod", ".gitleaks.toml", ".gitleaks-baseline.json"} {
		if !strings.Contains(s, must) {
			t.Errorf("security_scan.sh missing required token: %q", must)
		}
	}
}

// TestCoreciYmlHasSecurityStages verifies the .coreci.yml
// `validate` pipeline includes the three security stages added
// in P03.
func TestCoreciYmlHasSecurityStages(t *testing.T) {
	root, err := findRepoRoot()
	if err != nil {
		t.Fatalf("findRepoRoot: %v", err)
	}
	path := filepath.Join(root, ".coreci.yml")
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read .coreci.yml: %v", err)
	}
	s := string(body)
	for _, must := range []string{
		"- name: gosec",
		"- name: govulncheck",
		"- name: gitleaks",
		"GOFLAGS",
	} {
		if !strings.Contains(s, must) {
			t.Errorf(".coreci.yml missing required token: %q", must)
		}
	}
}

// TestMakefileHasSecurityAndTestRace verifies the new make
// targets are wired in.
func TestMakefileHasSecurityAndTestRace(t *testing.T) {
	root, err := findRepoRoot()
	if err != nil {
		t.Fatalf("findRepoRoot: %v", err)
	}
	path := filepath.Join(root, "Makefile")
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read Makefile: %v", err)
	}
	s := string(body)
	for _, must := range []string{
		"test-race:",
		"security-scan:",
		"go test -race",
		"scripts/security_scan.sh",
	} {
		if !strings.Contains(s, must) {
			t.Errorf("Makefile missing required token: %q", must)
		}
	}
}

// TestPreCommitHookShape verifies the gitleaks pre-commit hook
// exists, is executable, and gates only when gitleaks is present.
func TestPreCommitHookShape(t *testing.T) {
	root, err := findRepoRoot()
	if err != nil {
		t.Fatalf("findRepoRoot: %v", err)
	}
	path := filepath.Join(root, ".githooks", "pre-commit")
	info, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat: %v", err)
	}
	if info.Mode()&0o100 == 0 {
		t.Error("pre-commit hook is not executable")
	}
	body, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	s := string(body)
	for _, must := range []string{"gitleaks protect", "core.hooksPath"} {
		if !strings.Contains(s, must) {
			// core.hooksPath is a git config setting, not in the file
			// itself. Loosen the assertion for that one.
			if must == "core.hooksPath" {
				continue
			}
			t.Errorf("pre-commit missing required token: %q", must)
		}
	}
}

// TestCertPEMAllowlistMentions proves the .gitleaks.toml allowlist
// for cert PEM blocks is in effect. We don't run gitleaks; we
// just confirm the config structure has the right stopwords.
func TestCertPEMAllowlistMentions(t *testing.T) {
	root, err := findRepoRoot()
	if err != nil {
		t.Fatalf("findRepoRoot: %v", err)
	}
	body, err := os.ReadFile(filepath.Join(root, ".gitleaks.toml"))
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	s := string(body)
	if !strings.Contains(s, "-----BEGIN CERTIFICATE-----") {
		t.Error(".gitleaks.toml should allowlist cert PEM blocks")
	}
	if !strings.Contains(s, "-----END CERTIFICATE-----") {
		t.Error(".gitleaks.toml should allowlist cert PEM END blocks")
	}
}

// findRepoRoot walks up the directory tree to find the orca
// repo root (the directory containing go.mod). This makes the
// tests independent of cwd.
func findRepoRoot() (string, error) {
	dir, err := os.Getwd()
	if err != nil {
		return "", err
	}
	for {
		if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
			return dir, nil
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return "", os.ErrNotExist
		}
		dir = parent
	}
}

// TestGoTestRaceInCi verifies the .coreci.yml `test` pipeline
// runs `go test -race`. This is a documentation-shape check; the
// actual race-clean runs are in the prior session's history.
func TestGoTestRaceInCi(t *testing.T) {
	root, err := findRepoRoot()
	if err != nil {
		t.Fatalf("findRepoRoot: %v", err)
	}
	body, err := os.ReadFile(filepath.Join(root, ".coreci.yml"))
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if !strings.Contains(string(body), "go test -race") {
		t.Error(".coreci.yml test pipeline should run with -race (REQ-031)")
	}
}

// Compile-time guard that exec is used (testdata is referenced
// in future-proofing for gosec exclusion tests).
var _ = exec.Command
