package service

import (
	"context"
	"errors"
	"sync"
	"testing"
	"time"

	"wucher/internal/domain/audit"
)

type mockAuditRepo struct {
	mu          sync.Mutex
	entries     []*audit.AuditLog
	createCalls int
	createErr   error
	listRows    []audit.AuditLog
	listTotal   int64
	listErr     error
	lastFilter  string
	lastMethods []string
	lastModules []string
	lastActions []string
	lastSort    string
	lastLimit   int
	lastOffset  int
}

func (m *mockAuditRepo) Create(_ context.Context, entry *audit.AuditLog) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.createCalls++
	cp := *entry
	m.entries = append(m.entries, &cp)
	return m.createErr
}

func (m *mockAuditRepo) List(_ context.Context, filter string, methods []string, modules []string, actions []string, sort string, limit, offset int) ([]audit.AuditLog, int64, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.lastFilter = filter
	m.lastMethods = append([]string(nil), methods...)
	m.lastModules = append([]string(nil), modules...)
	m.lastActions = append([]string(nil), actions...)
	m.lastSort = sort
	m.lastLimit = limit
	m.lastOffset = offset
	if m.listErr != nil {
		return nil, 0, m.listErr
	}
	out := make([]audit.AuditLog, len(m.listRows))
	copy(out, m.listRows)
	return out, m.listTotal, nil
}

func TestAuditLogService_LogAsync(t *testing.T) {
	repo := &mockAuditRepo{}
	svc := NewAuditLogService(repo, AuditLogServiceDependencies{QueueSize: 8, Workers: 1, IPEncryptionKey: "pepper"})
	defer close(svc.queue)

	svc.Log(context.Background(), AuditLogInput{
		RequestID:  "req-1",
		Layer:      "service",
		Action:     "auth.reset_password.consume",
		Success:    true,
		StatusCode: 200,
		IP:         "127.0.0.1",
		Message:    "ok",
		Metadata: map[string]any{
			"k": "v",
		},
	})

	deadline := time.Now().Add(500 * time.Millisecond)
	for time.Now().Before(deadline) {
		repo.mu.Lock()
		n := len(repo.entries)
		repo.mu.Unlock()
		if n > 0 {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}

	repo.mu.Lock()
	defer repo.mu.Unlock()
	if len(repo.entries) != 1 {
		t.Fatalf("expected 1 audit entry, got %d", len(repo.entries))
	}
	if repo.entries[0].Action != "auth.reset_password.consume" {
		t.Fatalf("unexpected action: %s", repo.entries[0].Action)
	}
	if repo.entries[0].IP == "127.0.0.1" {
		t.Fatalf("expected encrypted ip to be persisted")
	}
	if plain := decryptStoredIP(repo.entries[0].IP, newIPSecretProtector("pepper")); plain != "127.0.0.1" {
		t.Fatalf("unexpected decrypted ip: %s", plain)
	}
}

func TestAuditLogService_NewAuditLogService_Defaults(t *testing.T) {
	repo := &mockAuditRepo{}
	svc := NewAuditLogService(repo, AuditLogServiceDependencies{QueueSize: 0, Workers: 0})
	defer close(svc.queue)

	if cap(svc.queue) != 1024 {
		t.Fatalf("expected default queue size 1024, got %d", cap(svc.queue))
	}

	svc.Log(context.Background(), AuditLogInput{
		Layer:  "service",
		Action: "audit.defaults",
	})

	deadline := time.Now().Add(500 * time.Millisecond)
	for time.Now().Before(deadline) {
		repo.mu.Lock()
		n := len(repo.entries)
		repo.mu.Unlock()
		if n > 0 {
			return
		}
		time.Sleep(10 * time.Millisecond)
	}

	t.Fatalf("expected default workers to process queued entry")
}

func TestAuditLogService_Log_NoopAndQueueBehavior(t *testing.T) {
	t.Run("no-op when receiver is nil", func(t *testing.T) {
		var svc *AuditLogService
		svc.Log(context.Background(), AuditLogInput{Layer: "service", Action: "noop"})
	})

	t.Run("no-op when repo is nil", func(t *testing.T) {
		svc := &AuditLogService{repo: nil, queue: make(chan *audit.AuditLog, 1)}
		svc.Log(context.Background(), AuditLogInput{Layer: "service", Action: "noop"})
		if len(svc.queue) != 0 {
			t.Fatalf("expected no queued entries when repo is nil")
		}
	})

	t.Run("metadata marshal failure does not panic and leaves metadata empty", func(t *testing.T) {
		svc := &AuditLogService{
			repo:  &mockAuditRepo{},
			queue: make(chan *audit.AuditLog, 1),
		}
		svc.Log(context.Background(), AuditLogInput{
			Layer:    "service",
			Action:   "audit.bad_metadata",
			Metadata: map[string]any{"bad": func() {}},
		})

		if len(svc.queue) != 1 {
			t.Fatalf("expected one queued entry")
		}
		entry := <-svc.queue
		if len(entry.Metadata) != 0 {
			t.Fatalf("expected metadata to be empty when marshal fails")
		}
	})

	t.Run("queue full branch drops extra entry", func(t *testing.T) {
		svc := &AuditLogService{
			repo:  &mockAuditRepo{},
			queue: make(chan *audit.AuditLog, 1),
		}
		svc.Log(context.Background(), AuditLogInput{Layer: "service", Action: "first"})
		svc.Log(context.Background(), AuditLogInput{Layer: "service", Action: "second"})

		if len(svc.queue) != 1 {
			t.Fatalf("expected queue to contain only first entry, got %d", len(svc.queue))
		}
		entry := <-svc.queue
		if entry.Action != "first" {
			t.Fatalf("expected first entry to be retained, got %q", entry.Action)
		}
	})
}

func TestAuditLogService_runWorker_NilEntryAndPersistError(t *testing.T) {
	repo := &mockAuditRepo{createErr: errors.New("persist failed")}
	svc := &AuditLogService{
		repo:  repo,
		queue: make(chan *audit.AuditLog, 2),
	}

	done := make(chan struct{})
	go func() {
		svc.runWorker()
		close(done)
	}()

	svc.queue <- nil
	svc.queue <- &audit.AuditLog{
		Layer:  "service",
		Action: "audit.persist_error",
	}
	close(svc.queue)

	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatalf("worker did not stop after queue close")
	}

	repo.mu.Lock()
	defer repo.mu.Unlock()
	if repo.createCalls != 1 {
		t.Fatalf("expected exactly one repo.Create call for non-nil entry, got %d", repo.createCalls)
	}
	if len(repo.entries) != 1 {
		t.Fatalf("expected one persisted entry attempt, got %d", len(repo.entries))
	}
	if repo.entries[0].Action != "audit.persist_error" {
		t.Fatalf("unexpected persisted action: %s", repo.entries[0].Action)
	}
}

func TestTruncateBytes(t *testing.T) {
	original := []byte("abcdef")

	t.Run("max less than or equal zero returns original bytes", func(t *testing.T) {
		got := truncateBytes(original, 0)
		if string(got) != string(original) {
			t.Fatalf("expected %q, got %q", string(original), string(got))
		}
	})

	t.Run("len less than or equal max returns original bytes", func(t *testing.T) {
		got := truncateBytes(original, len(original))
		if string(got) != string(original) {
			t.Fatalf("expected %q, got %q", string(original), string(got))
		}
	})

	t.Run("len greater than max returns truncated bytes", func(t *testing.T) {
		got := truncateBytes(original, 3)
		if string(got) != "abc" {
			t.Fatalf("expected %q, got %q", "abc", string(got))
		}
		if len(got) != 3 {
			t.Fatalf("expected len 3, got %d", len(got))
		}
	})
}

func TestAuditLogService_List(t *testing.T) {
	encIP := sanitizeIP("127.0.0.1", newIPSecretProtector("pepper"))
	repo := &mockAuditRepo{
		listRows: []audit.AuditLog{
			{Layer: "api", Action: "http.request", IP: encIP},
		},
		listTotal: 1,
	}
	svc := NewAuditLogService(repo, AuditLogServiceDependencies{QueueSize: 8, Workers: 1, IPEncryptionKey: "pepper"})
	defer close(svc.queue)

	rows, total, err := svc.List(context.Background(), "http", []string{"GET", "update"}, []string{"helicopters"}, []string{"list"}, "-created_at", 20, 0)
	if err != nil {
		t.Fatalf("List returned error: %v", err)
	}
	if total != 1 || len(rows) != 1 {
		t.Fatalf("unexpected list result total=%d len=%d", total, len(rows))
	}
	if rows[0].IP != "127.0.0.1" {
		t.Fatalf("expected decrypted ip in list response, got %q", rows[0].IP)
	}

	repo.mu.Lock()
	defer repo.mu.Unlock()
	if repo.lastSort != "created_at DESC" {
		t.Fatalf("expected normalized sort created_at DESC, got %q", repo.lastSort)
	}
	if repo.lastFilter != "http" {
		t.Fatalf("expected filter propagated, got %q", repo.lastFilter)
	}
	if len(repo.lastMethods) != 2 || repo.lastMethods[0] != "PUT" || repo.lastMethods[1] != "PATCH" {
		t.Fatalf("expected normalized methods [PUT PATCH], got %#v", repo.lastMethods)
	}
	if len(repo.lastModules) != 1 || repo.lastModules[0] != "helicopters" {
		t.Fatalf("expected modules [helicopters], got %#v", repo.lastModules)
	}
	if len(repo.lastActions) != 3 || repo.lastActions[0] != "create" || repo.lastActions[1] != "update" || repo.lastActions[2] != "delete" {
		t.Fatalf("expected actions [create update delete], got %#v", repo.lastActions)
	}
}
