package handlers

import (
	"bytes"
	"context"
	"encoding/hex"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/valyala/fasthttp"

	"wucher/internal/domain/base"
	"wucher/internal/domain/contact"
	"wucher/internal/domain/dul"
	filemanager "wucher/internal/domain/file_manager"
	filemanagerrealtime "wucher/internal/realtime/filemanager"
	"wucher/internal/shared/pkg/uuidv7"
)

type dulServiceMock struct {
	createFn   func(context.Context, *dul.DUL) error
	createErr  error
	updateErr  error
	deleteErr  error
	getErr     error
	listErr    error
	getResult  *dul.DUL
	listRows   []dul.DUL
	listTotal  int64
	lastListID [][]byte
}

func (m *dulServiceMock) Create(ctx context.Context, row *dul.DUL) error {
	if m.createFn != nil {
		return m.createFn(ctx, row)
	}
	if m.createErr != nil {
		return m.createErr
	}
	if len(row.ID) == 0 {
		row.ID = uuidv7.MustBytes()
	}
	return nil
}
func (m *dulServiceMock) Update(_ context.Context, _ *dul.DUL) error { return m.updateErr }
func (m *dulServiceMock) Delete(_ context.Context, _ []byte, _ []byte) error {
	return m.deleteErr
}
func (m *dulServiceMock) GetByID(_ context.Context, _ []byte) (*dul.DUL, error) {
	if m.getErr != nil {
		return nil, m.getErr
	}
	return m.getResult, nil
}
func (m *dulServiceMock) List(_ context.Context, _ string, _ string, _ int, _ int, baseIDs [][]byte) ([]dul.DUL, int64, error) {
	m.lastListID = baseIDs
	if m.listErr != nil {
		return nil, 0, m.listErr
	}
	return m.listRows, m.listTotal, nil
}

type baseServiceMock struct {
	listRows []base.Base
	listErr  error
}

func (m *baseServiceMock) CreateBase(_ context.Context, _ *base.Base, _ string) error { return nil }
func (m *baseServiceMock) UpdateBase(_ context.Context, _ *base.Base, _ string) error { return nil }
func (m *baseServiceMock) DeleteBase(_ context.Context, _ []byte, _ string) (*base.Base, error) {
	return nil, nil
}
func (m *baseServiceMock) GetBaseByID(_ context.Context, _ []byte, _ string) (*base.Base, error) {
	return nil, nil
}
func (m *baseServiceMock) ListBases(_ context.Context, _, _ string, _, _ int, _ string, _ *bool) ([]base.Base, int64, error) {
	if m.listErr != nil {
		return nil, 0, m.listErr
	}
	return m.listRows, int64(len(m.listRows)), nil
}

type contactServiceMock struct {
	isAdmin  bool
	adminErr error
	item     *contact.ContactListItem
	getErr   error
}

func (m *contactServiceMock) ListContacts(_ context.Context, _ contact.ListFilter) ([]contact.ContactListItem, int64, error) {
	return nil, 0, nil
}
func (m *contactServiceMock) GetContactByID(_ context.Context, _ []byte) (*contact.ContactListItem, error) {
	return m.item, m.getErr
}
func (m *contactServiceMock) IsActorAdmin(_ context.Context) (bool, error) {
	return m.isAdmin, m.adminErr
}
func (m *contactServiceMock) ResolveRoleCodeByID(_ context.Context, _ []byte) (string, error) {
	return "", nil
}
func (m *contactServiceMock) CreateContact(_ context.Context, _ contact.CreateInput) ([]byte, error) {
	return nil, nil
}
func (m *contactServiceMock) UpdateContact(_ context.Context, _ contact.UpdateInput) error {
	return nil
}
func (m *contactServiceMock) DeleteContact(_ context.Context, _ []byte) error { return nil }
func (m *contactServiceMock) UpdateContactStatus(_ context.Context, _ []byte, _ bool) error {
	return nil
}
func (m *contactServiceMock) UpdateContactRole(_ context.Context, _ []byte, _ string) error {
	return nil
}
func (m *contactServiceMock) UpdatePilotCategory(_ context.Context, _ []byte, _ string) error {
	return nil
}
func (m *contactServiceMock) UpdateLicense(_ context.Context, _ []byte, _ *string) error {
	return nil
}
func (m *contactServiceMock) UpdateChiefFlags(_ context.Context, _ []byte, _ contact.ProfilePatch) error {
	return nil
}
func (m *contactServiceMock) UpdateResponsibleComplaints(_ context.Context, _ []byte, _ bool) error {
	return nil
}
func (m *contactServiceMock) BulkUpdateStatus(_ context.Context, _ [][]byte, _ bool) (int64, error) {
	return 0, nil
}
func (m *contactServiceMock) CheckSensitiveAction(_ context.Context, _ []byte, _, _ string) error {
	return nil
}

func newDULApp(h *DULHandler) *fiber.App {
	app := fiber.New()
	app.Post("/api/v1/dul/create", func(c *fiber.Ctx) error {
		c.Locals("user_id", uuidv7.MustBytes())
		return h.Create(c)
	})
	app.Patch("/api/v1/dul/update/:id", func(c *fiber.Ctx) error {
		c.Locals("user_id", uuidv7.MustBytes())
		return h.Update(c)
	})
	app.Delete("/api/v1/dul/delete/:id", func(c *fiber.Ctx) error {
		c.Locals("user_id", uuidv7.MustBytes())
		return h.Delete(c)
	})
	app.Get("/api/v1/dul/get/:id", h.Get)
	app.Get("/api/v1/dul/get-all", func(c *fiber.Ctx) error {
		c.Locals("user_id", uuidv7.MustBytes())
		return h.List(c)
	})
	app.Get("/api/v1/dul/get-all/dt", func(c *fiber.Ctx) error {
		c.Locals("user_id", uuidv7.MustBytes())
		return h.ListDatatable(c)
	})
	return app
}

func newDULEventsApp(h *FileManagerEventsHandler, actor []byte) *fiber.App {
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		if actor != nil {
			c.Locals("user_id", append([]byte(nil), actor...))
		}
		return c.Next()
	})
	app.Get("/api/v1/dul/events", h.Stream)
	return app
}

func doReqDUL(t *testing.T, app *fiber.App, method, path string, body []byte) (*http.Response, map[string]any) {
	t.Helper()
	req, err := http.NewRequest(method, path, bytes.NewReader(body))
	if err != nil {
		t.Fatalf("new request: %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app test: %v", err)
	}
	raw, _ := io.ReadAll(resp.Body)
	out := map[string]any{}
	_ = json.Unmarshal(raw, &out)
	return resp, out
}

func TestDULHandler_CRUDAndList(t *testing.T) {
	baseID := uuidv7.MustBytes()
	baseIDStr, _ := uuidv7.BytesToString(baseID)
	dulID := uuidv7.MustBytes()
	dulIDStr, _ := uuidv7.BytesToString(dulID)

	svc := &dulServiceMock{
		getResult: &dul.DUL{
			ID:        dulID,
			BaseID:    baseID,
			Name:      "DUL A",
			Date:      time.Date(2026, 5, 25, 0, 0, 0, 0, time.UTC),
			CreatedAt: time.Now().UTC(),
			UpdatedAt: time.Now().UTC(),
			No:        1,
		},
		listRows: []dul.DUL{{
			ID:        dulID,
			BaseID:    baseID,
			Name:      "DUL A",
			Date:      time.Date(2026, 5, 25, 0, 0, 0, 0, time.UTC),
			CreatedAt: time.Now().UTC(),
			UpdatedAt: time.Now().UTC(),
			No:        1,
		}},
		listTotal: 1,
	}
	bsvc := &baseServiceMock{
		listRows: []base.Base{{
			ID:       baseID,
			BaseName: "Halim",
			DUL:      true,
			BaseCategory: base.BaseCategory{
				Name: "hems",
			},
		}},
	}
	h := NewDULHandler(svc).WithBaseService(bsvc)
	app := newDULApp(h)

	createBody := []byte(`{"data":{"type":"dul_create","attributes":{"name":"DUL A","date":"2026-05-25","base_id":"` + baseIDStr + `"}}}`)
	resp, _ := doReqDUL(t, app, http.MethodPost, "/api/v1/dul/create", createBody)
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("expected 201, got %d", resp.StatusCode)
	}

	updateBody := []byte(`{"data":{"type":"dul_update","attributes":{"name":"DUL B"}}}`)
	resp, _ = doReqDUL(t, app, http.MethodPatch, "/api/v1/dul/update/"+dulIDStr, updateBody)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}

	resp, _ = doReqDUL(t, app, http.MethodGet, "/api/v1/dul/get/"+dulIDStr, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200 get, got %d", resp.StatusCode)
	}

	resp, body := doReqDUL(t, app, http.MethodGet, "/api/v1/dul/get-all?page=1&size=20", nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200 list, got %d", resp.StatusCode)
	}
	meta, _ := body["meta"].(map[string]any)
	if meta == nil || meta["total_bases"] == nil {
		t.Fatalf("expected meta total_bases")
	}

	resp, body = doReqDUL(t, app, http.MethodGet, "/api/v1/dul/get-all/dt?page=1&size=20&draw=1", nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200 dt, got %d", resp.StatusCode)
	}
	meta, _ = body["meta"].(map[string]any)
	if meta == nil || meta["records_total"] == nil || meta["page_number"] == nil {
		t.Fatalf("expected datatable meta fields")
	}

	resp, _ = doReqDUL(t, app, http.MethodDelete, "/api/v1/dul/delete/"+dulIDStr, nil)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200 delete, got %d", resp.StatusCode)
	}
}

func TestDULHandler_CreateUpdate_FileUUIDFlow(t *testing.T) {
	baseID := uuidv7.MustBytes()
	baseIDStr, _ := uuidv7.BytesToString(baseID)
	dulID := uuidv7.MustBytes()
	dulIDStr, _ := uuidv7.BytesToString(dulID)
	fileID1 := uuidv7.MustBytes()
	fileID1Str, _ := uuidv7.BytesToString(fileID1)
	fileID2 := uuidv7.MustBytes()
	fileID2Str, _ := uuidv7.BytesToString(fileID2)

	svc := &dulServiceMock{
		getResult: &dul.DUL{
			ID:        dulID,
			BaseID:    baseID,
			Name:      "DUL A",
			Date:      time.Date(2026, 5, 26, 0, 0, 0, 0, time.UTC),
			CreatedAt: time.Now().UTC(),
			UpdatedAt: time.Now().UTC(),
			No:        1,
		},
	}

	createCalls := make([]filemanager.CreateAttachmentParams, 0, 2)
	mockFM := &fileManagerAttachmentHandlerServiceMock{
		getAttachmentFn: func(_ context.Context, _ []byte) (*filemanager.Attachment, error) {
			return nil, nil
		},
		getFileFn: func(_ context.Context, id []byte) (*filemanager.File, error) {
			return &filemanager.File{ID: append([]byte(nil), id...), Status: filemanager.FileStatusReady}, nil
		},
		listAttachmentsFn: func(context.Context, filemanager.ListAttachmentsParams) ([]filemanager.Attachment, int64, error) {
			return nil, 0, nil
		},
		createAttachmentFn: func(_ context.Context, p filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
			createCalls = append(createCalls, p)
			return &filemanager.Attachment{ID: uuidv7.MustBytes(), FileID: append([]byte(nil), p.FileID...)}, nil
		},
	}

	h := NewDULHandler(svc).WithFileManagerService(mockFM)
	app := newDULApp(h)

	t.Run("create accepts file_uuid", func(t *testing.T) {
		body := []byte(`{"data":{"type":"dul_create","attributes":{"name":"DUL A","date":"2026-05-26","base_id":"` + baseIDStr + `","file_uuid":["` + fileID1Str + `"]}}}`)
		resp, _ := doReqDUL(t, app, http.MethodPost, "/api/v1/dul/create", body)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d", resp.StatusCode)
		}
		if len(createCalls) != 1 {
			t.Fatalf("expected 1 create attachment call, got %d", len(createCalls))
		}
		if !bytes.Equal(createCalls[0].FileID, fileID1) {
			t.Fatalf("expected first attachment file id = file_uuid[0]")
		}
	})

	t.Run("update accepts file_uuid", func(t *testing.T) {
		body := []byte(`{"data":{"type":"dul_update","attributes":{"file_uuid":["` + fileID2Str + `"]}}}`)
		resp, _ := doReqDUL(t, app, http.MethodPatch, "/api/v1/dul/update/"+dulIDStr, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if len(createCalls) != 2 {
			t.Fatalf("expected 2 create attachment calls after update, got %d", len(createCalls))
		}
		if !bytes.Equal(createCalls[1].FileID, fileID2) {
			t.Fatalf("expected second attachment file id = file_uuid[0]")
		}
	})

	t.Run("update still supports image_attachment_ids for backward compatibility", func(t *testing.T) {
		fileID3 := uuidv7.MustBytes()
		fileID3Str, _ := uuidv7.BytesToString(fileID3)
		body := []byte(`{"data":{"type":"dul_update","attributes":{"image_attachment_ids":["` + fileID3Str + `"]}}}`)
		resp, _ := doReqDUL(t, app, http.MethodPatch, "/api/v1/dul/update/"+dulIDStr, body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
		if len(createCalls) != 3 {
			t.Fatalf("expected 3 create attachment calls after backward-compatible update, got %d", len(createCalls))
		}
		if !bytes.Equal(createCalls[2].FileID, fileID3) {
			t.Fatalf("expected third attachment file id from image_attachment_ids fallback")
		}
	})
}

func TestDULHandler_CreateRejectsNotReadyFileBeforeCreatingRow(t *testing.T) {
	baseID := uuidv7.MustBytes()
	baseIDStr, _ := uuidv7.BytesToString(baseID)
	fileID := uuidv7.MustBytes()
	fileIDStr, _ := uuidv7.BytesToString(fileID)
	created := false

	svc := &dulServiceMock{
		createFn: func(context.Context, *dul.DUL) error {
			created = true
			t.Fatal("DUL create must not be called when file is not ready")
			return nil
		},
	}
	mockFM := &fileManagerAttachmentHandlerServiceMock{
		getAttachmentFn: func(context.Context, []byte) (*filemanager.Attachment, error) {
			return nil, nil
		},
		getFileFn: func(_ context.Context, id []byte) (*filemanager.File, error) {
			if !bytes.Equal(id, fileID) {
				t.Fatalf("unexpected file id: %x", id)
			}
			return &filemanager.File{ID: append([]byte(nil), id...), Status: filemanager.FileStatusProcessing}, nil
		},
	}

	h := NewDULHandler(svc).WithFileManagerService(mockFM)
	app := newDULApp(h)

	body := []byte(`{"data":{"type":"dul_create","attributes":{"name":"DUL A","date":"2026-05-27","base_id":"` + baseIDStr + `","file_uuid":["` + fileIDStr + `"]}}}`)
	resp, respBody := doReqDUL(t, app, http.MethodPost, "/api/v1/dul/create", body)
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("expected 400 for not-ready file, got %d body=%v", resp.StatusCode, respBody)
	}
	if created {
		t.Fatal("expected DUL create to be skipped")
	}
}

func TestDULHandler_ListScope_AdminVsPilot(t *testing.T) {
	baseA := uuidv7.MustBytes()
	baseB := uuidv7.MustBytes()
	baseAHex := stringsLower(hex.EncodeToString(baseA))
	baseANameHex := stringsLower(hex.EncodeToString([]byte("Base A")))

	svc := &dulServiceMock{
		listRows:  []dul.DUL{},
		listTotal: 0,
	}
	bsvc := &baseServiceMock{
		listRows: []base.Base{
			{ID: baseA, BaseName: "Base A", DUL: true, BaseCategory: base.BaseCategory{Name: "hems"}},
			{ID: baseB, BaseName: "Base B", DUL: true, BaseCategory: base.BaseCategory{Name: "regular"}},
		},
	}

	// Admin => scoped to all bases where dul=true.
	hAdmin := NewDULHandler(svc).WithBaseService(bsvc).WithContactService(&contactServiceMock{isAdmin: true})
	app := fiber.New()
	app.Get("/list", func(c *fiber.Ctx) error {
		c.Locals("user_id", uuidv7.MustBytes())
		return hAdmin.List(c)
	})
	req, _ := http.NewRequest(http.MethodGet, "/list", nil)
	resp, _ := app.Test(req)
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("admin list status=%d", resp.StatusCode)
	}
	if len(svc.lastListID) != 2 {
		t.Fatalf("admin should see all dul bases, got scope size=%d", len(svc.lastListID))
	}

	// Pilot => scoped only to DUL base roles.
	raw := baseAHex + "|" + baseANameHex + "|dul|" + stringsLower(hex.EncodeToString([]byte("hems")))
	contactItem := &contact.ContactListItem{
		RoleCode:     "pilot",
		BaseRolesRaw: &raw,
	}
	hPilot := NewDULHandler(svc).WithBaseService(bsvc).WithContactService(&contactServiceMock{isAdmin: false, item: contactItem})
	app2 := fiber.New()
	app2.Get("/list", func(c *fiber.Ctx) error {
		c.Locals("user_id", uuidv7.MustBytes())
		return hPilot.List(c)
	})
	req2, _ := http.NewRequest(http.MethodGet, "/list", nil)
	resp2, _ := app2.Test(req2)
	if resp2.StatusCode != http.StatusOK {
		t.Fatalf("pilot list status=%d", resp2.StatusCode)
	}
	if len(svc.lastListID) != 1 {
		t.Fatalf("pilot should be scoped to one base, got %d", len(svc.lastListID))
	}
}

func TestDULHandler_ListScope_Rules(t *testing.T) {
	baseA := uuidv7.MustBytes()
	baseB := uuidv7.MustBytes()
	baseC := uuidv7.MustBytes()
	baseAHex := stringsLower(hex.EncodeToString(baseA))
	baseANameHex := stringsLower(hex.EncodeToString([]byte("Base A")))
	baseBHex := stringsLower(hex.EncodeToString(baseB))
	baseBNameHex := stringsLower(hex.EncodeToString([]byte("Base B")))
	makeApp := func(h *DULHandler) *fiber.App {
		app := fiber.New()
		app.Get("/list", func(c *fiber.Ctx) error {
			c.Locals("user_id", uuidv7.MustBytes())
			return h.List(c)
		})
		return app
	}
	newSvc := func() *dulServiceMock {
		return &dulServiceMock{listRows: []dul.DUL{}, listTotal: 0}
	}
	bsvc := &baseServiceMock{
		listRows: []base.Base{
			{ID: baseA, BaseName: "Base A", DUL: true, BaseCategory: base.BaseCategory{Name: "hems"}},
			{ID: baseB, BaseName: "Base B", DUL: true, BaseCategory: base.BaseCategory{Name: "regular"}},
			{ID: baseC, BaseName: "Base C", DUL: false, BaseCategory: base.BaseCategory{Name: "regular"}},
		},
	}

	t.Run("admin sees all dul=true bases", func(t *testing.T) {
		svc := newSvc()
		h := NewDULHandler(svc).WithBaseService(bsvc).WithContactService(&contactServiceMock{isAdmin: true})
		resp, _ := makeApp(h).Test(httpTestReq(http.MethodGet, "/list"))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("admin list status=%d", resp.StatusCode)
		}
		if len(svc.lastListID) != 2 {
			t.Fatalf("expected admin scope size=2 dul bases, got %d", len(svc.lastListID))
		}
	})

	t.Run("pilot sees only assigned dul bases", func(t *testing.T) {
		svc := newSvc()
		raw := baseAHex + "|" + baseANameHex + "|dul|" + stringsLower(hex.EncodeToString([]byte("hems"))) + "," +
			baseBHex + "|" + baseBNameHex + "|pilot|" + stringsLower(hex.EncodeToString([]byte("regular")))
		item := &contact.ContactListItem{RoleCode: "pilot", BaseRolesRaw: &raw}
		h := NewDULHandler(svc).WithBaseService(bsvc).WithContactService(&contactServiceMock{isAdmin: false, item: item})
		resp, _ := makeApp(h).Test(httpTestReq(http.MethodGet, "/list"))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("pilot list status=%d", resp.StatusCode)
		}
		if len(svc.lastListID) != 1 {
			t.Fatalf("expected pilot scope size=1 assigned dul base, got %d", len(svc.lastListID))
		}
	})

	t.Run("pilot with no assigned dul base gets empty list scope", func(t *testing.T) {
		svc := newSvc()
		raw := baseBHex + "|" + baseBNameHex + "|pilot|" + stringsLower(hex.EncodeToString([]byte("regular")))
		item := &contact.ContactListItem{RoleCode: "pilot", BaseRolesRaw: &raw}
		h := NewDULHandler(svc).WithBaseService(bsvc).WithContactService(&contactServiceMock{isAdmin: false, item: item})
		resp, _ := makeApp(h).Test(httpTestReq(http.MethodGet, "/list"))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("pilot list status=%d", resp.StatusCode)
		}
		if len(svc.lastListID) != 0 {
			t.Fatalf("expected pilot with no dul assignment scope size=0, got %d", len(svc.lastListID))
		}
	})

	t.Run("non pilot falls back to all dul=true bases", func(t *testing.T) {
		svc := newSvc()
		item := &contact.ContactListItem{RoleCode: "staff"}
		h := NewDULHandler(svc).WithBaseService(bsvc).WithContactService(&contactServiceMock{isAdmin: false, item: item})
		resp, _ := makeApp(h).Test(httpTestReq(http.MethodGet, "/list"))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("staff list status=%d", resp.StatusCode)
		}
		if len(svc.lastListID) != 2 {
			t.Fatalf("expected staff scope size=2 dul bases, got %d", len(svc.lastListID))
		}
	})

	t.Run("without contact service falls back to all dul=true bases", func(t *testing.T) {
		svc := newSvc()
		h := NewDULHandler(svc).WithBaseService(bsvc)
		resp, _ := makeApp(h).Test(httpTestReq(http.MethodGet, "/list"))
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("list status=%d", resp.StatusCode)
		}
		if len(svc.lastListID) != 2 {
			t.Fatalf("expected scope size=2 dul bases without contact service, got %d", len(svc.lastListID))
		}
	})

	t.Run("base service error returns 400", func(t *testing.T) {
		svc := newSvc()
		h := NewDULHandler(svc).WithBaseService(&baseServiceMock{listErr: errors.New("base boom")}).WithContactService(&contactServiceMock{isAdmin: true})
		resp, _ := makeApp(h).Test(httpTestReq(http.MethodGet, "/list"))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 when base scope resolution fails, got %d", resp.StatusCode)
		}
	})

	t.Run("contact admin check error returns 400", func(t *testing.T) {
		svc := newSvc()
		h := NewDULHandler(svc).WithBaseService(bsvc).WithContactService(&contactServiceMock{adminErr: errors.New("admin check boom")})
		resp, _ := makeApp(h).Test(httpTestReq(http.MethodGet, "/list"))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400 when admin check fails, got %d", resp.StatusCode)
		}
	})
}

func TestDULHandler_ValidationAndHelpers(t *testing.T) {
	svc := &dulServiceMock{createErr: errors.New("boom"), getResult: nil}
	h := NewDULHandler(svc)
	app := newDULApp(h)

	resp, body := doReqDUL(t, app, http.MethodPost, "/api/v1/dul/create", []byte(`{`))
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("expected 400 invalid json, got %d", resp.StatusCode)
	}
	errs, _ := body["errors"].([]any)
	if len(errs) == 0 {
		t.Fatalf("expected error payload")
	}

	if _, err := parseDULDate("2026-05-25"); err != nil {
		t.Fatalf("expected valid date")
	}
	if _, err := parseDULDate("25-05-2026"); err == nil {
		t.Fatalf("expected invalid date")
	}

	code, _ := inferDULErrorCode(fiber.StatusBadRequest, "Invalid JSON", "bad")
	if code != "DUL_INVALID_JSON" {
		t.Fatalf("unexpected code: %s", code)
	}
	code, _ = inferDULErrorCode(fiber.StatusUnprocessableEntity, "Validation error", "date must use format YYYY-MM-DD")
	if code != "DUL_DATE_INVALID" {
		t.Fatalf("unexpected code for date: %s", code)
	}
}

func stringsLower(v string) string { return strings.ToLower(v) }

func TestDULHelperFunctions(t *testing.T) {
	t.Run("constructor chains", func(t *testing.T) {
		h := NewDULHandler(&dulServiceMock{})
		h = h.WithBaseService(&baseServiceMock{})
		h = h.WithContactService(&contactServiceMock{})
		h = h.WithFileManagerService(nil)
		h = h.WithFileStorage(nil)
		if h == nil {
			t.Fatalf("expected handler not nil")
		}
	})

	t.Run("toResource and image mapping", func(t *testing.T) {
		now := time.Now().UTC()
		row := &dul.DUL{
			ID:        uuidv7.MustBytes(),
			BaseID:    uuidv7.MustBytes(),
			Name:      "DUL A",
			Date:      time.Date(2026, 5, 25, 0, 0, 0, 0, time.UTC),
			CreatedAt: now,
			UpdatedAt: now,
			No:        3,
			Base:      &base.Base{BaseName: "Halim", BaseCategory: base.BaseCategory{Name: "hems"}},
			Images: []*filemanager.Attachment{
				{ID: uuidv7.MustBytes()},
				nil,
			},
		}
		res := NewDULHandler(&dulServiceMock{}).toResource(row)
		if res.Type != "dul" || res.Attributes.Base.Name != "Halim" || len(res.Attributes.Images) != 1 {
			t.Fatalf("unexpected mapped resource")
		}
	})

	t.Run("inferDULErrorCode branches", func(t *testing.T) {
		cases := []struct {
			status int
			title  string
			detail string
		}{
			{fiber.StatusNotFound, "", ""},
			{fiber.StatusUnprocessableEntity, "Validation error", "date invalid"},
			{fiber.StatusUnprocessableEntity, "Validation error", "invalid uuid"},
			{fiber.StatusUnprocessableEntity, "Validation error", "something"},
			{fiber.StatusBadRequest, "Invalid JSON", ""},
			{fiber.StatusBadRequest, "Create failed", ""},
			{fiber.StatusBadRequest, "Update failed", ""},
			{fiber.StatusBadRequest, "Delete failed", ""},
			{fiber.StatusBadRequest, "List failed", ""},
			{fiber.StatusInternalServerError, "", ""},
		}
		for _, tc := range cases {
			code, errCode := inferDULErrorCode(tc.status, tc.title, tc.detail)
			if strings.TrimSpace(code) == "" || strings.TrimSpace(errCode) == "" {
				t.Fatalf("expected code for status=%d title=%q", tc.status, tc.title)
			}
		}
	})

	t.Run("isPilotRoleContact and parseDULBaseIDsFromRaw", func(t *testing.T) {
		if isPilotRoleContact(nil) {
			t.Fatalf("nil item should be false")
		}
		item := &contact.ContactListItem{RoleCode: "pilot"}
		if !isPilotRoleContact(item) {
			t.Fatalf("role_code pilot should be true")
		}
		rawRoles := "x|y|doctor," + mustRoleRawContact(t, uuidv7.MustBytes(), "Pilot", "pilot")
		item2 := &contact.ContactListItem{RolesRaw: &rawRoles}
		if !isPilotRoleContact(item2) {
			t.Fatalf("roles raw pilot should be true")
		}

		baseID := uuidv7.MustBytes()
		baseHex := stringsLower(hex.EncodeToString(baseID))
		nameHex := stringsLower(hex.EncodeToString([]byte("Base A")))
		raw := baseHex + "|" + nameHex + "|dul," + baseHex + "|" + nameHex + "|dul"
		ids, set := parseDULBaseIDsFromRaw(&raw)
		if len(ids) != 1 || len(set) != 1 {
			t.Fatalf("expected deduped dul base ids")
		}
		bad := "invalid"
		ids, set = parseDULBaseIDsFromRaw(&bad)
		if len(ids) != 0 || len(set) != 0 {
			t.Fatalf("invalid raw should produce empty ids")
		}
	})

	t.Run("syncImages branches", func(t *testing.T) {
		dulID := uuidv7.MustBytes()
		existingID := uuidv7.MustBytes()
		newFileID := uuidv7.MustBytes()
		newAttachmentID := uuidv7.MustBytes()
		created := 0

		mock := &fileManagerAttachmentHandlerServiceMock{
			listAttachmentsFn: func(context.Context, filemanager.ListAttachmentsParams) ([]filemanager.Attachment, int64, error) {
				return []filemanager.Attachment{{ID: existingID}}, 1, nil
			},
			deleteAttachmentFn: func(context.Context, []byte) error { return nil },
			getAttachmentFn: func(_ context.Context, _ []byte) (*filemanager.Attachment, error) {
				return &filemanager.Attachment{ID: newAttachmentID, FileID: newFileID}, nil
			},
			createAttachmentFn: func(context.Context, filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
				created++
				return &filemanager.Attachment{ID: uuidv7.MustBytes()}, nil
			},
		}
		h := NewDULHandler(&dulServiceMock{}).WithFileManagerService(mock)

		app := fiber.New()
		app.Get("/ok", func(c *fiber.Ctx) error {
			c.Locals("user_id", uuidv7.MustBytes())
			raw := mustUUIDStringContact(t, newAttachmentID)
			if err := h.syncImages(c, dulID, []string{raw}); err != nil {
				t.Fatalf("unexpected syncImages err: %v", err)
			}
			return nil
		})
		resp, err := app.Test(httpTestReq(http.MethodGet, "/ok"))
		if err != nil || resp.StatusCode != http.StatusOK || created != 1 {
			t.Fatalf("syncImages success not executed")
		}

		app2 := fiber.New()
		app2.Get("/bad", func(c *fiber.Ctx) error {
			err := h.syncImages(c, dulID, []string{"bad-uuid"})
			if err == nil {
				t.Fatalf("expected invalid uuid error")
			}
			return nil
		})
		if _, err := app2.Test(httpTestReq(http.MethodGet, "/bad")); err != nil {
			t.Fatalf("unexpected app2 err: %v", err)
		}

		t.Run("resubmit existing attachment uuid on update", func(t *testing.T) {
			// FE pulls the DUL via GET (which returns image.uuid = attachment id),
			// then PATCHes back the same uuid in image_attachment_ids. The bug we
			// fixed deleted the attachment first, then could not resolve the now-gone
			// uuid back to a file id and returned "image_attachment_ids[0] not found".
			existingAttID := uuidv7.MustBytes()
			existingFileID := uuidv7.MustBytes()
			deletedIDs := make([][]byte, 0, 1)
			createCalls := make([]filemanager.CreateAttachmentParams, 0, 1)
			lookupOrder := make([]string, 0, 2)

			mock := &fileManagerAttachmentHandlerServiceMock{
				listAttachmentsFn: func(context.Context, filemanager.ListAttachmentsParams) ([]filemanager.Attachment, int64, error) {
					lookupOrder = append(lookupOrder, "list")
					return []filemanager.Attachment{{ID: existingAttID, FileID: existingFileID}}, 1, nil
				},
				deleteAttachmentFn: func(_ context.Context, id []byte) error {
					lookupOrder = append(lookupOrder, "delete")
					deletedIDs = append(deletedIDs, append([]byte(nil), id...))
					return nil
				},
				getAttachmentFn: func(_ context.Context, id []byte) (*filemanager.Attachment, error) {
					lookupOrder = append(lookupOrder, "getAtt")
					if bytes.Equal(id, existingAttID) {
						return &filemanager.Attachment{ID: existingAttID, FileID: existingFileID}, nil
					}
					return nil, nil
				},
				getFileFn: func(_ context.Context, _ []byte) (*filemanager.File, error) {
					lookupOrder = append(lookupOrder, "getFile")
					return nil, nil
				},
				createAttachmentFn: func(_ context.Context, p filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
					lookupOrder = append(lookupOrder, "create")
					createCalls = append(createCalls, p)
					return &filemanager.Attachment{ID: uuidv7.MustBytes(), FileID: append([]byte(nil), p.FileID...)}, nil
				},
			}
			h := NewDULHandler(&dulServiceMock{}).WithFileManagerService(mock)
			dulID := uuidv7.MustBytes()

			app := fiber.New()
			app.Get("/resubmit", func(c *fiber.Ctx) error {
				c.Locals("user_id", uuidv7.MustBytes())
				raw := mustUUIDStringContact(t, existingAttID)
				if err := h.syncImages(c, dulID, []string{raw}); err != nil {
					t.Fatalf("syncImages must succeed when resubmitting existing attachment uuid: %v", err)
				}
				return nil
			})
			resp, err := app.Test(httpTestReq(http.MethodGet, "/resubmit"))
			if err != nil || resp.StatusCode != http.StatusOK {
				t.Fatalf("unexpected resubmit response: err=%v status=%d", err, resp.StatusCode)
			}
			if len(createCalls) != 1 {
				t.Fatalf("expected exactly one CreateAttachment call, got %d", len(createCalls))
			}
			if !bytes.Equal(createCalls[0].FileID, existingFileID) {
				t.Fatalf("CreateAttachment must reuse the original file id")
			}
			if len(deletedIDs) != 1 || !bytes.Equal(deletedIDs[0], existingAttID) {
				t.Fatalf("expected existing attachment to be deleted exactly once, got %v", deletedIDs)
			}
			// The lookup must happen BEFORE the delete; otherwise GetAttachmentByID
			// would race with the delete and return nil.
			getIdx, delIdx := -1, -1
			for i, step := range lookupOrder {
				if step == "getAtt" && getIdx == -1 {
					getIdx = i
				}
				if step == "delete" && delIdx == -1 {
					delIdx = i
				}
			}
			if getIdx == -1 || delIdx == -1 || getIdx > delIdx {
				t.Fatalf("expected GetAttachmentByID before DeleteAttachment, lookup order=%v", lookupOrder)
			}
		})

		t.Run("dedupe resubmitted file ids", func(t *testing.T) {
			// Sending the same file id twice must not violate the
			// (ref_type, ref_id, file_id) unique constraint at the attachments table.
			fileID := uuidv7.MustBytes()
			createCount := 0
			mock := &fileManagerAttachmentHandlerServiceMock{
				listAttachmentsFn: func(context.Context, filemanager.ListAttachmentsParams) ([]filemanager.Attachment, int64, error) {
					return nil, 0, nil
				},
				getAttachmentFn: func(_ context.Context, _ []byte) (*filemanager.Attachment, error) { return nil, nil },
				getFileFn: func(_ context.Context, id []byte) (*filemanager.File, error) {
					return &filemanager.File{ID: append([]byte(nil), id...), Status: filemanager.FileStatusReady}, nil
				},
				createAttachmentFn: func(_ context.Context, _ filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
					createCount++
					return &filemanager.Attachment{ID: uuidv7.MustBytes()}, nil
				},
			}
			h := NewDULHandler(&dulServiceMock{}).WithFileManagerService(mock)
			dulID := uuidv7.MustBytes()
			app := fiber.New()
			app.Get("/dedupe", func(c *fiber.Ctx) error {
				c.Locals("user_id", uuidv7.MustBytes())
				raw := mustUUIDStringContact(t, fileID)
				if err := h.syncImages(c, dulID, []string{raw, raw}); err != nil {
					t.Fatalf("unexpected err on duplicate ids: %v", err)
				}
				return nil
			})
			if _, err := app.Test(httpTestReq(http.MethodGet, "/dedupe")); err != nil {
				t.Fatalf("unexpected dedupe err: %v", err)
			}
			if createCount != 1 {
				t.Fatalf("duplicate ids must collapse to one CreateAttachment call, got %d", createCount)
			}
		})

		t.Run("accept file uuid fallback", func(t *testing.T) {
			createdFallback := 0
			mockFallback := &fileManagerAttachmentHandlerServiceMock{
				listAttachmentsFn: func(context.Context, filemanager.ListAttachmentsParams) ([]filemanager.Attachment, int64, error) {
					return nil, 0, nil
				},
				getAttachmentFn: func(_ context.Context, _ []byte) (*filemanager.Attachment, error) {
					return nil, nil
				},
				getFileFn: func(_ context.Context, id []byte) (*filemanager.File, error) {
					return &filemanager.File{ID: append([]byte(nil), id...), Status: filemanager.FileStatusReady}, nil
				},
				createAttachmentFn: func(_ context.Context, p filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) {
					createdFallback++
					return &filemanager.Attachment{ID: uuidv7.MustBytes(), FileID: append([]byte(nil), p.FileID...)}, nil
				},
			}
			hFallback := NewDULHandler(&dulServiceMock{}).WithFileManagerService(mockFallback)
			appFallback := fiber.New()
			appFallback.Get("/file-fallback", func(c *fiber.Ctx) error {
				c.Locals("user_id", uuidv7.MustBytes())
				raw := mustUUIDStringContact(t, newFileID)
				if err := hFallback.syncImages(c, dulID, []string{raw}); err != nil {
					t.Fatalf("unexpected fallback err: %v", err)
				}
				return nil
			})
			respFallback, err := appFallback.Test(httpTestReq(http.MethodGet, "/file-fallback"))
			if err != nil || respFallback.StatusCode != http.StatusOK || createdFallback != 1 {
				t.Fatalf("fallback path not executed")
			}
		})
	})
}

func TestDULEventsStream(t *testing.T) {
	hub := filemanagerrealtime.NewHub(1, nil)
	actor := uuidv7.MustBytes()
	userUUID, _ := uuidv7.BytesToString(actor)

	app := fiber.New()
	fctx := &fasthttp.RequestCtx{}
	fctx.Request.SetRequestURI("/api/v1/dul/events")
	c := app.AcquireCtx(fctx)
	c.Locals("user_id", actor)
	if err := NewFileManagerEventsHandler(hub).Stream(c); err != nil {
		t.Fatalf("unexpected stream setup error: %v", err)
	}

	hub.PublishToUsers([]string{userUUID}, filemanagerrealtime.FileStatusEvent{
		FileUUID:  mustUUIDStringContact(t, uuidv7.MustBytes()),
		Status:    "ready",
		Name:      "dul-image.png",
		MimeType:  "image/png",
		UpdatedAt: time.Now().UTC().Format(time.RFC3339),
	})
	go func() {
		time.Sleep(10 * time.Millisecond)
		closeFirstHubSubscriberForUser(hub, userUUID)
	}()

	bodyStr := c.Context().Response.String()
	app.ReleaseCtx(c)
	if !strings.Contains(bodyStr, ": connected") {
		t.Fatalf("expected connected comment in body: %q", bodyStr)
	}
	if !strings.Contains(bodyStr, "event: file.updated") {
		t.Fatalf("expected file.updated event in body: %q", bodyStr)
	}
	if !strings.Contains(bodyStr, "\"file_id\"") || !strings.Contains(bodyStr, "\"status\":\"ready\"") {
		t.Fatalf("expected file.updated payload in body: %q", bodyStr)
	}
}

func httpTestReq(method, path string) *http.Request {
	req, _ := http.NewRequest(method, path, nil)
	return req
}
