package handlers

import (
	"context"
	"errors"
	"io"
	"net/http"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"

	"wucher/internal/domain/audit"
	"wucher/internal/service"
	"wucher/internal/shared/pkg/uuidv7"
)

type mockAuditListRepo struct {
	mu          sync.Mutex
	rows        []audit.AuditLog
	total       int64
	err         error
	lastFilter  string
	lastMethods []string
	lastModules []string
	lastActions []string
	lastSort    string
	lastLimit   int
	lastOffset  int
}

func (m *mockAuditListRepo) Create(_ context.Context, _ *audit.AuditLog) error { return nil }

func (m *mockAuditListRepo) 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.err != nil {
		return nil, 0, m.err
	}
	out := make([]audit.AuditLog, len(m.rows))
	copy(out, m.rows)
	return out, m.total, nil
}

func TestAuditHandler_List(t *testing.T) {
	repo := &mockAuditListRepo{
		rows: []audit.AuditLog{
			{
				ID:         uuidv7.MustBytes(),
				Layer:      "api",
				Module:     "users",
				Action:     "list",
				Method:     "GET",
				Path:       "/api/v1/users/get-all",
				StatusCode: 200,
				Success:    true,
				IP:         "127.0.0.1",
				CreatedAt:  time.Now().UTC(),
			},
		},
		total: 1,
	}
	svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})

	h := NewAuditHandler(svc)
	app := fiber.New()
	app.Get("/api/v1/audit-logs/get-all", h.List)

	req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all?filter[search]=http&method=UPDATE&filter[module]=user&filter[action]=list&sort=-created_at&page[number]=1&page[size]=20", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}

	body, _ := io.ReadAll(resp.Body)
	bodyStr := string(body)
	if !containsAll(bodyStr, `"type":"audit_log"`, `"total":1`, `"ip":"127.0.0.1"`) {
		t.Fatalf("unexpected body: %s", bodyStr)
	}

	repo.mu.Lock()
	defer repo.mu.Unlock()
	if repo.lastFilter != "http" {
		t.Fatalf("expected filter to be propagated, got %q", repo.lastFilter)
	}
	if repo.lastSort != "created_at DESC" {
		t.Fatalf("expected normalized sort, got %q", repo.lastSort)
	}
	if len(repo.lastMethods) != 2 || repo.lastMethods[0] != "PUT" || repo.lastMethods[1] != "PATCH" {
		t.Fatalf("expected method filter [PUT PATCH], got %#v", repo.lastMethods)
	}
	if len(repo.lastModules) != 1 || repo.lastModules[0] != "user" {
		t.Fatalf("expected module filter [user], got %#v", repo.lastModules)
	}
	if len(repo.lastActions) != 3 || repo.lastActions[0] != "create" || repo.lastActions[1] != "update" || repo.lastActions[2] != "delete" {
		t.Fatalf("expected action filter [create update delete], got %#v", repo.lastActions)
	}
	if repo.lastLimit != 0 || repo.lastOffset != 0 {
		t.Fatalf("unexpected paging limit=%d offset=%d", repo.lastLimit, repo.lastOffset)
	}
}

func containsAll(v string, terms ...string) bool {
	for i := range terms {
		if !strings.Contains(v, terms[i]) {
			return false
		}
	}
	return true
}

func TestAuditHandler_List_Branches(t *testing.T) {
	t.Run("fallback filter action and clamp page", func(t *testing.T) {
		repo := &mockAuditListRepo{
			rows:  []audit.AuditLog{{ID: uuidv7.MustBytes(), Action: "create", CreatedAt: time.Now().UTC()}},
			total: 1,
		}
		svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})
		h := NewAuditHandler(svc)
		app := fiber.New()
		app.Get("/api/v1/audit-logs/get-all", h.List)

		req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all?filter[action]=create&page[number]=0&page[size]=500&sort=action", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}

		repo.mu.Lock()
		defer repo.mu.Unlock()
		if repo.lastFilter != "create" {
			t.Fatalf("expected action fallback filter, got %q", repo.lastFilter)
		}
		if repo.lastSort != "action ASC" {
			t.Fatalf("expected normalized sort, got %q", repo.lastSort)
		}
		if repo.lastLimit != 0 || repo.lastOffset != 0 {
			t.Fatalf("unexpected paging limit=%d offset=%d", repo.lastLimit, repo.lastOffset)
		}
	})

	t.Run("fallback filter layer", func(t *testing.T) {
		repo := &mockAuditListRepo{
			rows:  []audit.AuditLog{{ID: uuidv7.MustBytes(), Layer: "service", CreatedAt: time.Now().UTC()}},
			total: 1,
		}
		svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})
		h := NewAuditHandler(svc)
		app := fiber.New()
		app.Get("/api/v1/audit-logs/get-all", h.List)

		req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all?filter[layer]=service&page[number]=1&page[size]=20", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}

		repo.mu.Lock()
		defer repo.mu.Unlock()
		if repo.lastFilter != "service" {
			t.Fatalf("expected layer fallback filter, got %q", repo.lastFilter)
		}
	})

	t.Run("list error", func(t *testing.T) {
		repo := &mockAuditListRepo{err: errors.New("list failed")}
		svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})
		h := NewAuditHandler(svc)
		app := fiber.New()
		app.Get("/api/v1/audit-logs/get-all", h.List)

		req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("default mutation filter applied when query is empty", func(t *testing.T) {
		repo := &mockAuditListRepo{
			rows:  []audit.AuditLog{{ID: uuidv7.MustBytes(), Action: "update", Method: "PATCH", CreatedAt: time.Now().UTC()}},
			total: 1,
		}
		svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})
		h := NewAuditHandler(svc)
		app := fiber.New()
		app.Get("/api/v1/audit-logs/get-all", h.List)

		req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}

		repo.mu.Lock()
		defer repo.mu.Unlock()
		if len(repo.lastMethods) != 4 || repo.lastMethods[0] != "POST" || repo.lastMethods[1] != "PUT" || repo.lastMethods[2] != "PATCH" || repo.lastMethods[3] != "DELETE" {
			t.Fatalf("expected default method filter [POST PUT PATCH DELETE], got %#v", repo.lastMethods)
		}
		if len(repo.lastActions) != 3 || repo.lastActions[0] != "create" || repo.lastActions[1] != "update" || repo.lastActions[2] != "delete" {
			t.Fatalf("expected default action filter [create update delete], got %#v", repo.lastActions)
		}
	})

	t.Run("invalid module query returns 422", func(t *testing.T) {
		repo := &mockAuditListRepo{}
		svc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 8, Workers: 1})
		h := NewAuditHandler(svc)
		app := fiber.New()
		app.Get("/api/v1/audit-logs/get-all", h.List)

		req, _ := http.NewRequest(http.MethodGet, "/api/v1/audit-logs/get-all?module=bad-module", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})
}

func TestAuditLogResource_Branches(t *testing.T) {
	now := time.Now().UTC().Truncate(time.Second)
	row := &audit.AuditLog{
		ID:          uuidv7.MustBytes(),
		RequestID:   " req-1 ",
		ActorUserID: uuidv7.MustBytes(),
		Layer:       " service ",
		Action:      " create ",
		Method:      " POST ",
		Path:        " /api/v1/reserve-acs/get-all ",
		StatusCode:  201,
		Success:     true,
		IP:          " 127.0.0.1 ",
		UserAgent:   " ua ",
		Message:     " ok ",
		Metadata:    []byte(`{"module":"auth","count":2}`),
		CreatedAt:   now,
	}

	res := auditLogResource(row)
	if res.Type != "audit_log" || res.ID == "" {
		t.Fatalf("expected audit resource type/id")
	}
	if res.Attributes.ActorUserID == "" {
		t.Fatalf("expected actor_user_id to be encoded")
	}
	if res.Attributes.RequestID != "req-1" || res.Attributes.Layer != "service" || res.Attributes.Action != "create" {
		t.Fatalf("expected trimmed string attributes")
	}
	if res.Attributes.Module != "reserve_ac" {
		t.Fatalf("expected inferred module reserve_ac, got %q", res.Attributes.Module)
	}
	md, ok := res.Attributes.Metadata.(map[string]any)
	if !ok || md["module"] != "auth" {
		t.Fatalf("expected metadata map decoded, got %#v", res.Attributes.Metadata)
	}
	if res.Attributes.CreatedAt == "" {
		t.Fatalf("expected created_at in resource")
	}
}
