package api

import (
	"context"
	"net/http"
	"sync"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"

	"wucher/internal/domain/audit"
	"wucher/internal/service"
	sharedconst "wucher/internal/shared/const"
)

type mockAuditMiddlewareRepo struct {
	mu      sync.Mutex
	entries []*audit.AuditLog
}

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

func TestAuditMiddleware_SkipsGETRequests(t *testing.T) {
	repo := &mockAuditMiddlewareRepo{}
	auditSvc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 16, Workers: 1})

	app := fiber.New()
	app.Use(AuditMiddleware(auditSvc))
	app.Get("/api/v1/ping", func(c *fiber.Ctx) error {
		return c.SendStatus(http.StatusCreated)
	})

	req, _ := http.NewRequest(http.MethodGet, "/api/v1/ping", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("expected 201, got %d", resp.StatusCode)
	}

	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) != 0 {
		t.Fatalf("expected no audit log entry for GET, got=%d", len(repo.entries))
	}
}

func TestAuditMiddleware_LogsMutationRequests(t *testing.T) {
	repo := &mockAuditMiddlewareRepo{}
	auditSvc := service.NewAuditLogService(repo, service.AuditLogServiceDependencies{QueueSize: 16, Workers: 1})

	app := fiber.New()
	app.Use(AuditMiddleware(auditSvc))
	app.Post("/api/v1/helicopters/create", func(c *fiber.Ctx) error {
		return c.SendStatus(http.StatusCreated)
	})

	req, _ := http.NewRequest(http.MethodPost, "/api/v1/helicopters/create", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("expected 201, got %d", resp.StatusCode)
	}

	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) == 0 {
		t.Fatalf("expected audit log entry for mutation request")
	}
	got := repo.entries[0]
	if got.Layer != "api" || got.Module != sharedconst.AuditModuleHelicopter || got.Action != sharedconst.AuditActionCreate {
		t.Fatalf("unexpected audit entry: layer=%s module=%s action=%s", got.Layer, got.Module, got.Action)
	}
	if got.StatusCode != http.StatusCreated {
		t.Fatalf("expected status 201, got %d", got.StatusCode)
	}
}

func TestModuleFromPathSegment(t *testing.T) {
	cases := []struct {
		segment string
		want    string
	}{
		{segment: "helicopters", want: sharedconst.AuditModuleHelicopter},
		{segment: "bases", want: sharedconst.AuditModuleBase},
		{segment: "health-insurance-companies", want: sharedconst.AuditModuleHealthInsuranceCompany},
		{segment: "hems-roster", want: sharedconst.AuditModuleUnknown},
		{segment: "dul", want: sharedconst.AuditModuleDUL},
		{segment: "reserve-acs", want: sharedconst.AuditModuleReserveAC},
		{segment: "not-registered", want: sharedconst.AuditModuleUnknown},
	}

	for _, tc := range cases {
		t.Run(tc.segment, func(t *testing.T) {
			got := moduleFromPathSegment(tc.segment)
			if got != tc.want {
				t.Fatalf("moduleFromPathSegment(%q) = %q, want %q", tc.segment, got, tc.want)
			}
		})
	}
}

func TestResolveAuditModuleAction(t *testing.T) {
	cases := []struct {
		name       string
		method     string
		target     string
		wantModule string
		wantAction string
	}{
		{
			name:       "datatable list",
			method:     http.MethodGet,
			target:     "/api/v1/helicopters/get-all/dt",
			wantModule: sharedconst.AuditModuleHelicopter,
			wantAction: sharedconst.AuditActionList,
		},
		{
			name:       "explicit create",
			method:     http.MethodPost,
			target:     "/api/v1/helicopters/create",
			wantModule: sharedconst.AuditModuleHelicopter,
			wantAction: sharedconst.AuditActionCreate,
		},
		{
			name:       "fallback method update",
			method:     http.MethodPatch,
			target:     "/api/v1/helicopters/019dbeb6-4c37-7495-b0d0-e02ed134f87d",
			wantModule: sharedconst.AuditModuleHelicopter,
			wantAction: sharedconst.AuditActionUpdate,
		},
		{
			name:       "base regular variant from query",
			method:     http.MethodGet,
			target:     "/api/v1/bases/get-all?category_type=regular",
			wantModule: sharedconst.AuditModuleBaseRegular,
			wantAction: sharedconst.AuditActionList,
		},
		{
			name:       "base hems variant from query alias",
			method:     http.MethodGet,
			target:     "/api/v1/bases/get-all?base_category=hems",
			wantModule: sharedconst.AuditModuleBaseHEMS,
			wantAction: sharedconst.AuditActionList,
		},
		{
			name:       "unknown module",
			method:     http.MethodGet,
			target:     "/api/v1/ping",
			wantModule: sharedconst.AuditModuleUnknown,
			wantAction: sharedconst.AuditActionGet,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			app := fiber.New()
			app.All("/*", func(c *fiber.Ctx) error {
				gotModule, gotAction := resolveAuditModuleAction(c)
				if gotModule != tc.wantModule || gotAction != tc.wantAction {
					t.Fatalf("resolveAuditModuleAction(%q, %q) = (%q, %q), want (%q, %q)",
						tc.method, tc.target, gotModule, gotAction, tc.wantModule, tc.wantAction)
				}
				return c.SendStatus(http.StatusNoContent)
			})
			req, _ := http.NewRequest(tc.method, tc.target, nil)
			resp, err := app.Test(req)
			if err != nil {
				t.Fatalf("app.Test: %v", err)
			}
			if resp.StatusCode != http.StatusNoContent {
				t.Fatalf("expected 204, got %d", resp.StatusCode)
			}
		})
	}
}
