package handlers

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

	"github.com/gofiber/fiber/v2"

	flightprepcheck "wucher/internal/domain/flight_prep_check"
	"wucher/internal/shared/pkg/uuidv7"
)

type flightPrepServiceMock struct {
	initFn   func(context.Context, []byte) (int, error)
	getFn    func(context.Context, []byte) (*flightprepcheck.FlightPrepCheck, error)
	updateFn func(context.Context, []byte, *flightprepcheck.UpsertRequest) (*flightprepcheck.FlightPrepCheck, error)
	upsertFn func(context.Context, []byte, *flightprepcheck.UpsertRequest) (*flightprepcheck.FlightPrepCheck, error)
}

func (m *flightPrepServiceMock) InitializeChecks(ctx context.Context, id []byte) (int, error) {
	if m.initFn != nil {
		return m.initFn(ctx, id)
	}
	return 0, nil
}

func (m *flightPrepServiceMock) GetByFlightInspection(ctx context.Context, id []byte) (*flightprepcheck.FlightPrepCheck, error) {
	if m.getFn != nil {
		return m.getFn(ctx, id)
	}
	return nil, nil
}

func (m *flightPrepServiceMock) UpdateCheck(ctx context.Context, id []byte, req *flightprepcheck.UpsertRequest) (*flightprepcheck.FlightPrepCheck, error) {
	if m.updateFn != nil {
		return m.updateFn(ctx, id, req)
	}
	return nil, nil
}

func (m *flightPrepServiceMock) UpsertByFlightInspection(ctx context.Context, id []byte, req *flightprepcheck.UpsertRequest) (*flightprepcheck.FlightPrepCheck, error) {
	if m.upsertFn != nil {
		return m.upsertFn(ctx, id, req)
	}
	return nil, nil
}

type flightPrepRepoMock struct{}

func (flightPrepRepoMock) Create(context.Context, *flightprepcheck.FlightPrepCheck) error { return nil }
func (flightPrepRepoMock) Update(context.Context, *flightprepcheck.FlightPrepCheck) error { return nil }
func (flightPrepRepoMock) GetByID(context.Context, []byte) (*flightprepcheck.FlightPrepCheck, error) {
	return nil, nil
}
func (flightPrepRepoMock) GetByFlightInspectionID(context.Context, []byte) (*flightprepcheck.FlightPrepCheck, error) {
	return nil, nil
}

func newFlightPrepApp(h *FlightPrepCheckHandler) *fiber.App {
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_id", uuidv7.MustBytes())
		return c.Next()
	})
	app.Post("/inspection/:flight_inspection_id/init", h.InitializeChecks)
	app.Get("/inspection/:flight_inspection_id", h.ListChecks)
	app.Patch("/check/:check_id", h.UpdateCheck)
	app.Patch("/inspection/:flight_inspection_id/bulk", h.BulkUpdateChecks)
	return app
}

func mustUUIDStr(t *testing.T) string {
	t.Helper()
	s, err := uuidv7.String(uuidv7.MustNew())
	if err != nil {
		t.Fatalf("uuid: %v", err)
	}
	return s
}

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

func TestFlightPrepCheckHandler_New(t *testing.T) {
	h := NewFlightPrepCheckHandler(&flightPrepServiceMock{}, flightPrepRepoMock{})
	if h == nil || h.service == nil || h.repo == nil {
		t.Fatal("expected handler initialized")
	}
}

func TestFlightPrepCheckHandler_InitializeChecks(t *testing.T) {
	t.Run("invalid uuid", func(t *testing.T) {
		h := NewFlightPrepCheckHandler(&flightPrepServiceMock{}, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		resp, _ := doReq(t, app, http.MethodPost, "/inspection/bad/init", nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("validation error", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			initFn: func(context.Context, []byte) (int, error) {
				return 0, &flightprepcheck.ValidationError{Status: http.StatusUnprocessableEntity, Title: "X", Detail: "y"}
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		id := mustUUIDStr(t)
		resp, _ := doReq(t, app, http.MethodPost, "/inspection/"+id+"/init", nil)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("internal error", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			initFn: func(context.Context, []byte) (int, error) {
				return 0, errors.New("boom")
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		id := mustUUIDStr(t)
		resp, _ := doReq(t, app, http.MethodPost, "/inspection/"+id+"/init", nil)
		if resp.StatusCode != http.StatusInternalServerError {
			t.Fatalf("expected 500, got %d", resp.StatusCode)
		}
	})

	t.Run("success", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			initFn: func(context.Context, []byte) (int, error) { return 3, nil },
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		id := mustUUIDStr(t)
		resp, body := doReq(t, app, http.MethodPost, "/inspection/"+id+"/init", nil)
		if resp.StatusCode != http.StatusCreated {
			t.Fatalf("expected 201, got %d body=%s", resp.StatusCode, body)
		}
		if !strings.Contains(body, `"created":3`) {
			t.Fatalf("expected created=3 in body, got %s", body)
		}
	})
}

func TestFlightPrepCheckHandler_ListChecks(t *testing.T) {
	t.Run("invalid uuid", func(t *testing.T) {
		h := NewFlightPrepCheckHandler(&flightPrepServiceMock{}, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		resp, _ := doReq(t, app, http.MethodGet, "/inspection/bad", nil)
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("validation error", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			getFn: func(context.Context, []byte) (*flightprepcheck.FlightPrepCheck, error) {
				return nil, &flightprepcheck.ValidationError{Status: http.StatusUnprocessableEntity, Title: "X", Detail: "y"}
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		resp, _ := doReq(t, app, http.MethodGet, "/inspection/"+mustUUIDStr(t), nil)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("internal error", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			getFn: func(context.Context, []byte) (*flightprepcheck.FlightPrepCheck, error) {
				return nil, errors.New("boom")
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		resp, _ := doReq(t, app, http.MethodGet, "/inspection/"+mustUUIDStr(t), nil)
		if resp.StatusCode != http.StatusInternalServerError {
			t.Fatalf("expected 500, got %d", resp.StatusCode)
		}
	})

	t.Run("not found", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			getFn: func(context.Context, []byte) (*flightprepcheck.FlightPrepCheck, error) {
				return nil, nil
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		resp, _ := doReq(t, app, http.MethodGet, "/inspection/"+mustUUIDStr(t), nil)
		if resp.StatusCode != http.StatusNotFound {
			t.Fatalf("expected 404, got %d", resp.StatusCode)
		}
	})

	t.Run("success", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			getFn: func(context.Context, []byte) (*flightprepcheck.FlightPrepCheck, error) {
				return &flightprepcheck.FlightPrepCheck{
					ID:                 uuidv7.MustBytes(),
					FlightInspectionID: uuidv7.MustBytes(),
					NOTAMBriefing:      true,
					CreatedAt:          time.Now().UTC(),
					UpdatedAt:          time.Now().UTC(),
				}, nil
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		resp, body := doReq(t, app, http.MethodGet, "/inspection/"+mustUUIDStr(t), nil)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, body)
		}
		if !strings.Contains(body, `"flight_prep_check"`) {
			t.Fatalf("expected resource type in body: %s", body)
		}
	})
}

func TestFlightPrepCheckHandler_UpdateCheck(t *testing.T) {
	t.Run("invalid uuid", func(t *testing.T) {
		h := NewFlightPrepCheckHandler(&flightPrepServiceMock{}, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		resp, _ := doReq(t, app, http.MethodPatch, "/check/bad", []byte(`{}`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid json", func(t *testing.T) {
		h := NewFlightPrepCheckHandler(&flightPrepServiceMock{}, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		resp, _ := doReq(t, app, http.MethodPatch, "/check/"+mustUUIDStr(t), []byte(`{not-json`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("validation error", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			updateFn: func(context.Context, []byte, *flightprepcheck.UpsertRequest) (*flightprepcheck.FlightPrepCheck, error) {
				return nil, &flightprepcheck.ValidationError{Status: http.StatusUnprocessableEntity, Title: "X", Detail: "y"}
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		body := []byte(`{"data":{"type":"flight_prep_check_upsert","attributes":{"notam_briefing":true}}}`)
		resp, _ := doReq(t, app, http.MethodPatch, "/check/"+mustUUIDStr(t), body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("internal error", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			updateFn: func(context.Context, []byte, *flightprepcheck.UpsertRequest) (*flightprepcheck.FlightPrepCheck, error) {
				return nil, errors.New("boom")
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		body := []byte(`{"data":{"type":"flight_prep_check_upsert","attributes":{"notam_briefing":true}}}`)
		resp, _ := doReq(t, app, http.MethodPatch, "/check/"+mustUUIDStr(t), body)
		if resp.StatusCode != http.StatusInternalServerError {
			t.Fatalf("expected 500, got %d", resp.StatusCode)
		}
	})

	t.Run("success", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			updateFn: func(_ context.Context, _ []byte, r *flightprepcheck.UpsertRequest) (*flightprepcheck.FlightPrepCheck, error) {
				if r.NOTAMBriefing == nil || !*r.NOTAMBriefing {
					t.Fatal("expected NOTAMBriefing true")
				}
				return &flightprepcheck.FlightPrepCheck{ID: uuidv7.MustBytes(), FlightInspectionID: uuidv7.MustBytes()}, nil
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		body := []byte(`{"data":{"type":"flight_prep_check_upsert","attributes":{"notam_briefing":true,"weather_briefing":false,"operational_flight_plan":true}}}`)
		resp, b := doReq(t, app, http.MethodPatch, "/check/"+mustUUIDStr(t), body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, b)
		}
	})
}

func TestFlightPrepCheckHandler_BulkUpdateChecks(t *testing.T) {
	t.Run("invalid uuid", func(t *testing.T) {
		h := NewFlightPrepCheckHandler(&flightPrepServiceMock{}, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		resp, _ := doReq(t, app, http.MethodPatch, "/inspection/bad/bulk", []byte(`{}`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid json", func(t *testing.T) {
		h := NewFlightPrepCheckHandler(&flightPrepServiceMock{}, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		resp, _ := doReq(t, app, http.MethodPatch, "/inspection/"+mustUUIDStr(t)+"/bulk", []byte(`{not-json`))
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", resp.StatusCode)
		}
	})

	t.Run("validation error", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			upsertFn: func(context.Context, []byte, *flightprepcheck.UpsertRequest) (*flightprepcheck.FlightPrepCheck, error) {
				return nil, &flightprepcheck.ValidationError{Status: http.StatusUnprocessableEntity, Title: "X", Detail: "y"}
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		body := []byte(`{"data":{"type":"flight_prep_check_upsert","attributes":{"notam_briefing":true}}}`)
		resp, _ := doReq(t, app, http.MethodPatch, "/inspection/"+mustUUIDStr(t)+"/bulk", body)
		if resp.StatusCode != http.StatusUnprocessableEntity {
			t.Fatalf("expected 422, got %d", resp.StatusCode)
		}
	})

	t.Run("internal error", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			upsertFn: func(context.Context, []byte, *flightprepcheck.UpsertRequest) (*flightprepcheck.FlightPrepCheck, error) {
				return nil, errors.New("boom")
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		body := []byte(`{"data":{"type":"flight_prep_check_upsert","attributes":{"notam_briefing":true}}}`)
		resp, _ := doReq(t, app, http.MethodPatch, "/inspection/"+mustUUIDStr(t)+"/bulk", body)
		if resp.StatusCode != http.StatusInternalServerError {
			t.Fatalf("expected 500, got %d", resp.StatusCode)
		}
	})

	t.Run("success", func(t *testing.T) {
		svc := &flightPrepServiceMock{
			upsertFn: func(context.Context, []byte, *flightprepcheck.UpsertRequest) (*flightprepcheck.FlightPrepCheck, error) {
				return &flightprepcheck.FlightPrepCheck{ID: uuidv7.MustBytes(), FlightInspectionID: uuidv7.MustBytes()}, nil
			},
		}
		h := NewFlightPrepCheckHandler(svc, flightPrepRepoMock{})
		app := newFlightPrepApp(h)
		body := []byte(`{"data":{"type":"flight_prep_check_upsert","attributes":{"notam_briefing":true}}}`)
		resp, b := doReq(t, app, http.MethodPatch, "/inspection/"+mustUUIDStr(t)+"/bulk", body)
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, b)
		}
	})
}
