package drift

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"testing"
	"time"

	"golang.org/x/crypto/hkdf"
)

type mockTransport struct {
	execOut []byte
	execErr error
	execFn  func(ctx context.Context, peer string, cmd string) ([]byte, error)
	readOut []byte
	readErr error
	writes  []writeEntry
}

type writeEntry struct {
	peer    string
	path    string
	content []byte
	mode    os.FileMode
}

func (m *mockTransport) Exec(ctx context.Context, peer string, cmd string) ([]byte, error) {
	if m.execFn != nil {
		return m.execFn(ctx, peer, cmd)
	}
	return m.execOut, m.execErr
}

func (m *mockTransport) WriteFileIdempotent(ctx context.Context, peer string, path string, content []byte, mode os.FileMode) (bool, error) {
	m.writes = append(m.writes, writeEntry{peer, path, content, mode})
	return true, nil
}

func (m *mockTransport) ReadFile(ctx context.Context, peer string, path string) ([]byte, error) {
	return m.readOut, m.readErr
}

func setupDriftTestEnv(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	t.Setenv("ORCA_LEAD_STATE_DIR", dir)
	return dir
}

func TestParseEvent(t *testing.T) {
	raw := []byte(`{
		"event_id":"EVT-1",
		"ts":"2026-01-01T00:00:00Z",
		"host":"peer1",
		"path":"/etc/traefik/dynamic/orca.yml",
		"status":"modified",
		"new_sha256":"abc",
		"latest_txn":"T-1234567890abcdef",
		"expected_sha256":"def",
		"drift_confirmed":true,
		"action":"reported",
		"action_result":"skipped",
		"operator_id":"op1"
	}`)
	e, err := ParseEvent(raw)
	if err != nil {
		t.Fatalf("ParseEvent: %v", err)
	}
	if e.EventID != "EVT-1" {
		t.Errorf("EventID = %q, want EVT-1", e.EventID)
	}
	if e.Host != "peer1" {
		t.Errorf("Host = %q, want peer1", e.Host)
	}
	if e.Path != "/etc/traefik/dynamic/orca.yml" {
		t.Errorf("Path = %q", e.Path)
	}
	if e.Status != StatusModified {
		t.Errorf("Status = %q, want modified", e.Status)
	}
	if !e.DriftConfirmed {
		t.Errorf("DriftConfirmed = false, want true")
	}
	if e.Action != ActionReported {
		t.Errorf("Action = %q, want reported", e.Action)
	}
	if e.ActionResult != ActionResultSkipped {
		t.Errorf("ActionResult = %q, want skipped", e.ActionResult)
	}
}

func TestParseEventInvalid(t *testing.T) {
	_, err := ParseEvent([]byte(`{not json`))
	if err == nil {
		t.Fatal("expected error for invalid JSON, got nil")
	}
}

func TestMarshalEventRoundTrip(t *testing.T) {
	e := Event{EventID: "E1", Host: "h", Path: "/p", Status: StatusCreated, DriftConfirmed: true}
	raw, err := MarshalEvent(e)
	if err != nil {
		t.Fatalf("MarshalEvent: %v", err)
	}
	out, err := ParseEvent(raw)
	if err != nil {
		t.Fatalf("ParseEvent: %v", err)
	}
	if out.EventID != e.EventID || out.Host != e.Host || out.Path != e.Path || out.Status != e.Status {
		t.Errorf("round-trip mismatch: %+v vs %+v", out, e)
	}
}

func TestLoadConfigDefault(t *testing.T) {
	cfg, err := LoadConfig("")
	if err != nil {
		t.Fatalf("LoadConfig empty: %v", err)
	}
	if !cfg.Polling.Enabled {
		t.Errorf("Polling.Enabled = false, want true")
	}
	if cfg.Polling.DefaultInterval != 60*time.Second {
		t.Errorf("DefaultInterval = %v, want 60s", cfg.Polling.DefaultInterval)
	}
	if len(cfg.Paths.Critical) == 0 {
		t.Errorf("Critical paths empty")
	}
	found := false
	for _, c := range cfg.Paths.Critical {
		if c.Pattern == "/etc/traefik/dynamic/orca.yml" {
			found = true
			if c.Interval != 5*time.Second {
				t.Errorf("critical interval = %v, want 5s", c.Interval)
			}
			if !c.SystemdPathUnit {
				t.Errorf("SystemdPathUnit = false, want true")
			}
		}
	}
	if !found {
		t.Errorf("traefik critical path missing")
	}
	found = false
	for _, s := range cfg.Paths.Standard {
		if s.Pattern == "/etc/orca/actual/*" {
			found = true
			if s.Interval != 30*time.Second {
				t.Errorf("standard interval = %v, want 30s", s.Interval)
			}
		}
	}
	if !found {
		t.Errorf("/etc/orca/actual/* standard path missing")
	}
	if !cfg.Remediate.Auto {
		t.Errorf("Remediate.Auto = false, want true")
	}
}

func TestLoadConfigFile(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "drift.json")
	cfgJSON := `{
		"polling": {"enabled": true, "default_interval": 90000000000, "max_concurrent_peers": 4},
		"paths": {
			"critical": [{"tier":"critical","pattern":"/etc/critical.yml","interval":5000000000,"systemd_path_unit":true}],
			"standard": [{"tier":"standard","pattern":"/etc/standard.yml","interval":30000000000}],
			"excluded": ["/run/orca/*"]
		},
		"remediate": {"auto": false, "auto_paths": [], "require_approval": ["/etc/critical.yml"], "notify_on_remediation": false}
	}`
	if err := os.WriteFile(path, []byte(cfgJSON), 0o644); err != nil {
		t.Fatalf("write config: %v", err)
	}
	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}
	if cfg.Polling.MaxConcurrentPeers != 4 {
		t.Errorf("MaxConcurrentPeers = %d, want 4", cfg.Polling.MaxConcurrentPeers)
	}
	if cfg.Polling.DefaultInterval != 90*time.Second {
		t.Errorf("DefaultInterval = %v, want 90s", cfg.Polling.DefaultInterval)
	}
	if cfg.Remediate.Auto {
		t.Errorf("Auto = true, want false")
	}
	if len(cfg.Paths.Critical) != 1 || cfg.Paths.Critical[0].Pattern != "/etc/critical.yml" {
		t.Errorf("critical = %+v", cfg.Paths.Critical)
	}
}

func TestValidateConfigValid(t *testing.T) {
	cfg := DefaultConfig()
	if err := ValidateConfig(cfg); err != nil {
		t.Fatalf("ValidateConfig(default): %v", err)
	}
}

func TestValidateConfigEmptyPattern(t *testing.T) {
	cfg := DefaultConfig()
	cfg.Paths.Critical[0].Pattern = ""
	if err := ValidateConfig(cfg); err == nil {
		t.Fatal("expected error for empty critical pattern")
	}
}

func TestValidateConfigPollingZero(t *testing.T) {
	cfg := DefaultConfig()
	cfg.Polling.Enabled = true
	cfg.Polling.DefaultInterval = 0
	if err := ValidateConfig(cfg); err == nil {
		t.Fatal("expected error for zero default_interval with polling enabled")
	}
}

func TestValidateConfigOverlapCriticalExcluded(t *testing.T) {
	cfg := DefaultConfig()
	cfg.Paths.Excluded = append(cfg.Paths.Excluded, "/etc/traefik/dynamic/orca.yml")
	err := ValidateConfig(cfg)
	if err == nil {
		t.Fatal("expected overlap error")
	}
	if !errors.Is(err, ErrOverlapCritical) {
		t.Errorf("expected ErrOverlapCritical, got %v", err)
	}
}

func TestValidateConfigNil(t *testing.T) {
	if err := ValidateConfig(nil); err == nil {
		t.Fatal("expected error for nil config")
	}
}

func TestAggregateLocalMissing(t *testing.T) {
	setupDriftTestEnv(t)
	d := NewDefaultDetector(&mockTransport{})
	events, err := d.Aggregate(context.Background(), "")
	if err != nil {
		t.Fatalf("Aggregate missing: %v", err)
	}
	if events != nil {
		t.Errorf("events = %v, want nil", events)
	}
}

func TestAggregateLocal(t *testing.T) {
	setupDriftTestEnv(t)
	doc := `{"ts":"2026-01-01T00:00:00Z","events":[{"event_id":"E1","host":"p","path":"/etc/traefik/dynamic/orca.yml","status":"modified","drift_confirmed":true}]}`
	if err := os.WriteFile(AggregatedPath(), []byte(doc), 0o644); err != nil {
		t.Fatalf("write aggregated: %v", err)
	}
	d := NewDefaultDetector(&mockTransport{})
	events, err := d.Aggregate(context.Background(), "")
	if err != nil {
		t.Fatalf("Aggregate: %v", err)
	}
	if len(events) != 1 {
		t.Fatalf("events len = %d, want 1", len(events))
	}
	if events[0].EventID != "E1" {
		t.Errorf("EventID = %q, want E1", events[0].EventID)
	}
}

func TestAggregateRemote(t *testing.T) {
	setupDriftTestEnv(t)
	doc := `{"ts":"2026-01-01T00:00:00Z","events":[{"event_id":"E2","host":"p","path":"/etc/orca/actual/ns-a/x","status":"created","drift_confirmed":true}]}`
	mt := &mockTransport{readOut: []byte(doc)}
	d := NewDefaultDetector(mt)
	events, err := d.Aggregate(context.Background(), "lead:22")
	if err != nil {
		t.Fatalf("Aggregate remote: %v", err)
	}
	if len(events) != 1 {
		t.Fatalf("events len = %d, want 1", len(events))
	}
	if events[0].EventID != "E2" {
		t.Errorf("EventID = %q, want E2", events[0].EventID)
	}
}

func TestWatchCancels(t *testing.T) {
	setupDriftTestEnv(t)
	doc := `{"ts":"2026-01-01T00:00:00Z","events":[{"event_id":"E1","host":"p","path":"/etc/traefik/dynamic/orca.yml","status":"modified","drift_confirmed":true}]}`
	if err := os.WriteFile(AggregatedPath(), []byte(doc), 0o644); err != nil {
		t.Fatalf("write aggregated: %v", err)
	}
	d := NewDefaultDetector(&mockTransport{})
	d.SetCooldown(50 * time.Millisecond)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	gotEvents := 0
	for e, err := range d.Watch(ctx, []PathSpec{{Pattern: "/etc/traefik/dynamic/orca.yml", Interval: 10 * time.Millisecond}}) {
		if err != nil {
			t.Fatalf("Watch err: %v", err)
		}
		gotEvents++
		_ = e
		cancel()
		break
	}
	if gotEvents != 1 {
		t.Errorf("gotEvents = %d, want 1", gotEvents)
	}
}

func TestWatchStreamsAndDedupes(t *testing.T) {
	setupDriftTestEnv(t)
	doc := `{"ts":"2026-01-01T00:00:00Z","events":[{"event_id":"E1","host":"p","path":"/etc/traefik/dynamic/orca.yml","status":"modified","drift_confirmed":true}]}`
	if err := os.WriteFile(AggregatedPath(), []byte(doc), 0o644); err != nil {
		t.Fatalf("write aggregated: %v", err)
	}
	d := NewDefaultDetector(&mockTransport{})
	d.SetCooldown(50 * time.Millisecond)
	ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
	defer cancel()
	got := 0
	for _, err := range d.Watch(ctx, nil) {
		if err != nil {
			t.Fatalf("Watch err: %v", err)
		}
		got++
	}
	if got != 1 {
		t.Errorf("got = %d, want 1 (dedup)", got)
	}
}

func TestRemediateCooldownOnSuccess(t *testing.T) {
	setupDriftTestEnv(t)
	mt := &mockTransport{execOut: []byte("ok")}
	d := NewDefaultDetector(mt)
	d.SetCooldown(1 * time.Hour)
	if err := d.Remediate(context.Background(), "lead:22", "/etc/traefik/dynamic/orca.yml", false); err != nil {
		t.Fatalf("first Remediate: %v", err)
	}
	if err := d.Remediate(context.Background(), "lead:22", "/etc/traefik/dynamic/orca.yml", false); err == nil {
		t.Fatal("second Remediate should hit cooldown")
	} else if !errors.Is(err, ErrCooldown) {
		t.Errorf("expected ErrCooldown, got %v", err)
	}
}

func TestRemediateForceBypassesCooldown(t *testing.T) {
	setupDriftTestEnv(t)
	mt := &mockTransport{execOut: []byte("ok")}
	d := NewDefaultDetector(mt)
	d.SetCooldown(1 * time.Hour)
	if err := d.Remediate(context.Background(), "lead:22", "/etc/p", false); err != nil {
		t.Fatalf("first: %v", err)
	}
	if err := d.Remediate(context.Background(), "lead:22", "/etc/p", true); err != nil {
		t.Fatalf("force bypass: %v", err)
	}
}

func TestRemediateTransientNoCooldown(t *testing.T) {
	setupDriftTestEnv(t)
	mt := &mockTransport{execErr: fmt.Errorf("connection refused")}
	d := NewDefaultDetector(mt)
	d.SetCooldown(1 * time.Hour)
	if err := d.Remediate(context.Background(), "lead:22", "/etc/p", false); err == nil {
		t.Fatal("expected transient error")
	}
	if d.inCooldown("/etc/p") {
		t.Errorf("cooldown entered after transient failure")
	}
}

func TestRemediateRequiresLead(t *testing.T) {
	setupDriftTestEnv(t)
	d := NewDefaultDetector(&mockTransport{})
	if err := d.Remediate(context.Background(), "", "/etc/p", true); err == nil {
		t.Fatal("expected error for empty lead")
	}
}

func TestAcknowledgeWrites(t *testing.T) {
	setupDriftTestEnv(t)
	mt := &mockTransport{}
	d := NewDefaultDetector(mt)
	if err := d.Acknowledge(context.Background(), "lead:22", "/etc/traefik/dynamic/orca.yml"); err != nil {
		t.Fatalf("Acknowledge: %v", err)
	}
	if len(mt.writes) != 1 {
		t.Fatalf("writes = %d, want 1", len(mt.writes))
	}
	w := mt.writes[0]
	if w.peer != "lead:22" {
		t.Errorf("peer = %q, want lead:22", w.peer)
	}
	if w.path != AcknowledgmentsPath() {
		t.Errorf("path = %q, want %q", w.path, AcknowledgmentsPath())
	}
}

func TestAcknowledgeRequiresLead(t *testing.T) {
	setupDriftTestEnv(t)
	d := NewDefaultDetector(&mockTransport{})
	if err := d.Acknowledge(context.Background(), "", "/etc/p"); err == nil {
		t.Fatal("expected error for empty lead")
	}
}

func TestNamespaceScopeClusterWide(t *testing.T) {
	events := []Event{
		{EventID: "E1", Path: "/etc/traefik/dynamic/orca.yml", DriftConfirmed: true},
		{EventID: "E2", Path: "/etc/orca/actual/ns-a/x", DriftConfirmed: true},
		{EventID: "E3", Path: "/etc/orca/actual/ns-b/y", DriftConfirmed: true, Action: ActionAcknowledged},
	}
	out := NamespaceScope(events, "")
	if len(out) != 2 {
		t.Errorf("cluster-wide scope len = %d, want 2 (ack excluded)", len(out))
	}
}

func TestNamespaceScopePerNamespace(t *testing.T) {
	events := []Event{
		{EventID: "E1", Path: "/etc/orca/actual/ns-a/x", DriftConfirmed: true},
		{EventID: "E2", Path: "/etc/orca/actual/ns-b/y", DriftConfirmed: true},
		{EventID: "E3", Path: "/etc/traefik/dynamic/orca.yml", DriftConfirmed: true},
	}
	nsA := NamespaceScope(events, "ns-a")
	if len(nsA) != 1 || nsA[0].EventID != "E1" {
		t.Errorf("ns-a scope = %+v, want only E1", nsA)
	}
	nsB := NamespaceScope(events, "ns-b")
	if len(nsB) != 1 || nsB[0].EventID != "E2" {
		t.Errorf("ns-b scope = %+v, want only E2", nsB)
	}
}

func TestExpandBraces(t *testing.T) {
	out := expandBraces("/etc/x.{service,timer}")
	if len(out) != 2 {
		t.Fatalf("len = %d, want 2", len(out))
	}
	if out[0] != "/etc/x.service" || out[1] != "/etc/x.timer" {
		t.Errorf("out = %v", out)
	}
}

func TestPathGlobMatch(t *testing.T) {
	if !pathGlobMatch("/etc/orca/actual/*", "/etc/orca/actual/ns-a") {
		t.Errorf("expected glob match for single segment")
	}
	if !pathGlobMatch("/etc/orca/actual/*", "/etc/orca/actual/ns-a/x") {
		t.Errorf("expected glob match for nested segment (drift /* crosses /)")
	}
	if !pathGlobMatch("/etc/systemd/system/orca-collector.{service,timer}", "/etc/systemd/system/orca-collector.service") {
		t.Errorf("expected brace match")
	}
}

func TestNsForPath(t *testing.T) {
	if got := nsForPath("/etc/orca/actual/ns-a/x"); got != "ns-a" {
		t.Errorf("nsForPath = %q, want ns-a", got)
	}
	if got := nsForPath("/etc/traefik/dynamic/orca.yml"); got != "" {
		t.Errorf("nsForPath = %q, want empty", got)
	}
}

// --- REQ-140 / F18 drift event authentication test ---

// TestVerifyEventSignature verifies HMAC verification works.
func TestVerifyEventSignature(t *testing.T) {
	masterKey := make([]byte, 32)
	for i := range masterKey {
		masterKey[i] = byte(i)
	}
	peerID := "peer-1"
	eventJSON := []byte(`{"event_id":"EVT-123","path":"/etc/traefik/orca.yaml","status":"changed"}`)
	// Compute a valid signature.
	hk := hkdf.New(sha256.New, masterKey, []byte(peerID), []byte("orca-drift-event-hmac"))
	key := make([]byte, 32)
	hk.Read(key)
	mac := hmac.New(sha256.New, key)
	mac.Write(eventJSON)
	sig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
	if !VerifyEventSignature(eventJSON, sig, masterKey, peerID) {
		t.Error("valid signature should verify")
	}
	// Wrong key.
	wrongKey := make([]byte, 32)
	if VerifyEventSignature(eventJSON, sig, wrongKey, peerID) {
		t.Error("wrong key should fail")
	}
	// Wrong peer.
	if VerifyEventSignature(eventJSON, sig, masterKey, "wrong-peer") {
		t.Error("wrong peer should fail")
	}
	// Tampered event.
	tampered := append([]byte{}, eventJSON...)
	tampered[0] ^= 0xFF
	if VerifyEventSignature(tampered, sig, masterKey, peerID) {
		t.Error("tampered event should fail")
	}
	// Empty signature.
	if VerifyEventSignature(eventJSON, "", masterKey, peerID) {
		t.Error("empty signature should fail")
	}
}
