package handlers

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

	"github.com/gofiber/fiber/v2"

	"wucher/internal/domain/mcf"
	"wucher/internal/shared/pkg/uuidv7"
)

type stubMCFSvc struct {
	created   *mcf.MaintenanceCheckFlight
	updated   *mcf.MaintenanceCheckFlight
	latest    *mcf.MaintenanceCheckFlight
	byID      *mcf.MaintenanceCheckFlight
	createErr error
	updateErr error
	deleteErr error
	listErr   error
	latestErr error
	hasOpen   bool
}

type stubHistoryRec struct{ n int }

func (s *stubHistoryRec) Record(_ context.Context, _ []byte, _ string, _ []byte, _, _ string, _ []byte) error {
	s.n++
	return nil
}
func (s *stubHistoryRec) RecordByInspection(_ context.Context, _ []byte, _ string, _ []byte, _, _ string, _ []byte) error {
	return nil
}

func (s *stubMCFSvc) Create(_ context.Context, row *mcf.MaintenanceCheckFlight) error {
	if len(row.ID) == 0 {
		row.ID = uuidv7.MustBytes()
	}
	s.created = row
	return s.createErr
}
func (s *stubMCFSvc) Update(_ context.Context, row *mcf.MaintenanceCheckFlight) error {
	s.updated = row
	return s.updateErr
}
func (s *stubMCFSvc) Delete(_ context.Context, _, _ []byte) error { return s.deleteErr }
func (s *stubMCFSvc) GetByID(_ context.Context, _ []byte) (*mcf.MaintenanceCheckFlight, error) {
	if s.byID != nil {
		return s.byID, nil
	}
	if s.updated != nil {
		return s.updated, nil
	}
	return s.created, nil
}
func (s *stubMCFSvc) ListByHelicopter(_ context.Context, _ []byte) ([]mcf.MaintenanceCheckFlight, error) {
	if s.listErr != nil {
		return nil, s.listErr
	}
	if s.latest != nil {
		return []mcf.MaintenanceCheckFlight{*s.latest}, nil
	}
	return nil, nil
}
func (s *stubMCFSvc) GetLatestByHelicopter(_ context.Context, _ []byte) (*mcf.MaintenanceCheckFlight, error) {
	return s.latest, s.latestErr
}
func (s *stubMCFSvc) PendingHelicopterIDs(_ context.Context, _ [][]byte) (map[string]bool, error) {
	return nil, nil
}

func (s *stubMCFSvc) LatestByHelicopterIDs(_ context.Context, _ [][]byte) (map[string]*mcf.MaintenanceCheckFlight, error) {
	return nil, nil
}

func (s *stubMCFSvc) HelicopterHasOpenMCF(_ context.Context, _ []byte) (bool, error) {
	return s.hasOpen, nil
}

func newMCFApp(svc mcf.Service) *fiber.App {
	h := NewMCFHandler(svc)
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_id", []byte("actor0123456789a"))
		return c.Next()
	})
	app.Post("/mcf/set/:helicopter_id", h.Set)
	app.Post("/mcf/sign/:id", h.Sign)
	app.Get("/mcf/get/:id", h.GetByID)
	app.Get("/mcf/get-by-helicopter/:helicopter_id", h.ListByHelicopter)
	return app
}

func doMCF(t *testing.T, app *fiber.App, method, path, body string) (int, map[string]any) {
	t.Helper()
	req, _ := http.NewRequest(method, path, bytes.NewReader([]byte(body)))
	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)
	var out map[string]any
	_ = json.Unmarshal(raw, &out)
	return resp.StatusCode, out
}

func mcfUUID() string {
	s, _ := uuidv7.BytesToString(uuidv7.MustBytes())
	return s
}

func openMCF() *mcf.MaintenanceCheckFlight {
	return &mcf.MaintenanceCheckFlight{ID: uuidv7.MustBytes(), HelicopterID: uuidv7.MustBytes()}
}

func TestMCFHandlerSet(t *testing.T) {
	hid := mcfUUID()

	t.Run("activate creates a draft", func(t *testing.T) {
		svc := &stubMCFSvc{}
		code, _ := doMCF(t, newMCFApp(svc), http.MethodPost, "/mcf/set/"+hid,
			`{"data":{"type":"mcf_set","attributes":{"active":true}}}`)
		if code != http.StatusCreated {
			t.Fatalf("want 201, got %d", code)
		}
		if svc.created == nil || len(svc.created.HelicopterID) == 0 {
			t.Fatal("activate should persist a draft with helicopter id")
		}
		if svc.created.Result != nil || svc.created.CompletedAt != nil {
			t.Fatal("a draft must not be completed/signed")
		}
	})

	t.Run("omitted active defaults to activate", func(t *testing.T) {
		svc := &stubMCFSvc{}
		code, _ := doMCF(t, newMCFApp(svc), http.MethodPost, "/mcf/set/"+hid,
			`{"data":{"type":"mcf_set","attributes":{}}}`)
		if code != http.StatusCreated || svc.created == nil {
			t.Fatalf("omitted active should create a draft, got %d %#v", code, svc.created)
		}
	})

	t.Run("activate with flight_id stores it", func(t *testing.T) {
		svc := &stubMCFSvc{}
		fid := mcfUUID()
		code, _ := doMCF(t, newMCFApp(svc), http.MethodPost, "/mcf/set/"+hid,
			`{"data":{"type":"mcf_set","attributes":{"active":true,"flight_id":"`+fid+`"}}}`)
		if code != http.StatusCreated || svc.created == nil || len(svc.created.FlightID) != 16 {
			t.Fatalf("activate should persist flight id, got %d %#v", code, svc.created)
		}
	})

	t.Run("activate updates the open draft (in place)", func(t *testing.T) {
		draft := &mcf.MaintenanceCheckFlight{ID: uuidv7.MustBytes(), HelicopterID: uuidv7.MustBytes()} // open draft
		svc := &stubMCFSvc{latest: draft}
		code, _ := doMCF(t, newMCFApp(svc), http.MethodPost, "/mcf/set/"+hid,
			`{"data":{"type":"mcf_set","attributes":{"active":true,"af_hours":"1245.3","landing_count":3}}}`)
		if code != http.StatusOK {
			t.Fatalf("want 200 (update open draft), got %d", code)
		}
		if svc.updated == nil || svc.updated.AFHours != "1245.3" || svc.updated.LandingCount != 3 {
			t.Fatalf("activate should update the open draft with the data, got %#v", svc.updated)
		}
		if svc.updated.CompletedAt != nil {
			t.Fatalf("updating a draft must not sign it, got %#v", svc.updated)
		}
		if svc.created != nil {
			t.Fatal("must not create a second MCF when a draft is already open")
		}
	})

	t.Run("deactivate cancels the open draft", func(t *testing.T) {
		draft := &mcf.MaintenanceCheckFlight{ID: uuidv7.MustBytes(), HelicopterID: uuidv7.MustBytes()} // open draft
		svc := &stubMCFSvc{latest: draft}
		code, _ := doMCF(t, newMCFApp(svc), http.MethodPost, "/mcf/set/"+hid,
			`{"data":{"type":"mcf_set","attributes":{"active":false}}}`)
		if code != http.StatusOK {
			t.Fatalf("want 200, got %d", code)
		}
		if svc.updated == nil || svc.updated.CancelledAt == nil || len(svc.updated.CancelledBy) == 0 {
			t.Fatalf("deactivate must mark the draft cancelled, got %#v", svc.updated)
		}
	})

	t.Run("deactivate does not cancel a signed MCF", func(t *testing.T) {
		done := time.Now().UTC()
		signed := &mcf.MaintenanceCheckFlight{ID: uuidv7.MustBytes(), HelicopterID: uuidv7.MustBytes(), CompletedAt: &done}
		svc := &stubMCFSvc{latest: signed}
		code, _ := doMCF(t, newMCFApp(svc), http.MethodPost, "/mcf/set/"+hid,
			`{"data":{"type":"mcf_set","attributes":{"active":false}}}`)
		if code != http.StatusOK {
			t.Fatalf("want 200, got %d", code)
		}
		if svc.updated != nil {
			t.Fatalf("a signed MCF (real event) must not be cancelled, got %#v", svc.updated)
		}
	})

	t.Run("deactivate no-op when nothing open", func(t *testing.T) {
		svc := &stubMCFSvc{latest: nil}
		code, _ := doMCF(t, newMCFApp(svc), http.MethodPost, "/mcf/set/"+hid,
			`{"data":{"type":"mcf_set","attributes":{"active":false}}}`)
		if code != http.StatusOK {
			t.Fatalf("want 200, got %d", code)
		}
		if svc.updated != nil {
			t.Fatal("no open MCF -> nothing to cancel")
		}
	})

	t.Run("invalid helicopter_id path -> 422", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{}), http.MethodPost, "/mcf/set/not-a-uuid",
			`{"data":{"type":"mcf_set","attributes":{"active":true}}}`)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("want 422, got %d", code)
		}
	})

	t.Run("invalid json -> 400", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{}), http.MethodPost, "/mcf/set/"+hid, `{"data":`)
		if code != http.StatusBadRequest {
			t.Fatalf("want 400, got %d", code)
		}
	})

	t.Run("wrong data type -> INVALID_DATA_TYPE", func(t *testing.T) {
		// flight_id is a string; sending a number is a data-type mismatch.
		code, body := doMCF(t, newMCFApp(&stubMCFSvc{}), http.MethodPost, "/mcf/set/"+hid,
			`{"data":{"type":"mcf_set","attributes":{"active":true,"flight_id":123}}}`)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("want 422, got %d body=%#v", code, body)
		}
		errs, _ := body["errors"].([]any)
		if len(errs) == 0 || errs[0].(map[string]any)["code"] != "INVALID_DATA_TYPE" {
			t.Fatalf("expected INVALID_DATA_TYPE, got %#v", body)
		}
	})
}

func completeBody(result string) string {
	return `{"data":{"type":"mcf","attributes":{"af_hours":"1245.3","landing_count":3,"utc_time":"2026-06-17T08:30:00Z","date":"2026-06-17","result":"` + result + `"}}}`
}

func TestMCFHandlerSign(t *testing.T) {
	id := mcfUUID()

	t.Run("sign passed clears", func(t *testing.T) {
		svc := &stubMCFSvc{byID: openMCF()}
		code, _ := doMCF(t, newMCFApp(svc), http.MethodPost, "/mcf/sign/"+id, completeBody("passed"))
		if code != http.StatusOK {
			t.Fatalf("want 200, got %d", code)
		}
		if svc.updated == nil || svc.updated.Result == nil || *svc.updated.Result != mcf.ResultPassed {
			t.Fatal("expected result=passed persisted")
		}
		if svc.updated.CompletedAt == nil || svc.updated.CompletedBy == nil || svc.updated.Date == nil || svc.updated.LandingCount != 3 {
			t.Fatal("expected check-flight fields + signer stamped")
		}
	})

	t.Run("sign failed stays grounded", func(t *testing.T) {
		svc := &stubMCFSvc{byID: openMCF()}
		code, _ := doMCF(t, newMCFApp(svc), http.MethodPost, "/mcf/sign/"+id, completeBody("failed"))
		if code != http.StatusOK || svc.updated == nil || *svc.updated.Result != mcf.ResultFailed {
			t.Fatalf("want 200 with failed result, got %d", code)
		}
	})

	t.Run("unsign reverts to draft but keeps data", func(t *testing.T) {
		passed := mcf.ResultPassed
		now := time.Now().UTC()
		signed := openMCF()
		signed.Result = &passed
		signed.AFHours = "1300"
		signed.CompletedAt = &now
		signed.CompletedBy = []byte("actor0123456789a")
		svc := &stubMCFSvc{byID: signed}
		code, _ := doMCF(t, newMCFApp(svc), http.MethodPost, "/mcf/sign/"+id,
			`{"data":{"type":"mcf","attributes":{"sign":false}}}`)
		if code != http.StatusOK {
			t.Fatalf("want 200, got %d", code)
		}
		// completed_at cleared (back to draft), but the saved data is kept.
		if svc.updated == nil || svc.updated.CompletedAt != nil || len(svc.updated.CompletedBy) != 0 {
			t.Fatalf("unsign must clear completion, got %#v", svc.updated)
		}
		if svc.updated.Result == nil || *svc.updated.Result != mcf.ResultPassed || svc.updated.AFHours != "1300" {
			t.Fatalf("unsign must KEEP the saved data, got %#v", svc.updated)
		}
	})


	t.Run("sign on missing mcf -> 404", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{}), http.MethodPost, "/mcf/sign/"+id, completeBody("passed"))
		if code != http.StatusNotFound {
			t.Fatalf("want 404, got %d", code)
		}
	})

	t.Run("sign missing required field -> 422", func(t *testing.T) {
		body := `{"data":{"type":"mcf","attributes":{"sign":true,"landing_count":3,"utc_time":"2026-06-17T08:30:00Z","date":"2026-06-17","result":"passed"}}}` // no af_hours
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{byID: openMCF()}), http.MethodPost, "/mcf/sign/"+id, body)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("want 422, got %d", code)
		}
	})

	t.Run("sign invalid result -> 422", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{byID: openMCF()}), http.MethodPost, "/mcf/sign/"+id, completeBody("maybe"))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("want 422, got %d", code)
		}
	})

	t.Run("sign bad date -> 422", func(t *testing.T) {
		body := `{"data":{"type":"mcf","attributes":{"af_hours":"1","landing_count":1,"utc_time":"2026-06-17T08:30:00Z","date":"17-06-2026","result":"passed"}}}`
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{byID: openMCF()}), http.MethodPost, "/mcf/sign/"+id, body)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("want 422, got %d", code)
		}
	})

	t.Run("sign bad utc_time -> 422", func(t *testing.T) {
		body := `{"data":{"type":"mcf","attributes":{"af_hours":"1","landing_count":1,"utc_time":"nope","date":"2026-06-17","result":"passed"}}}`
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{byID: openMCF()}), http.MethodPost, "/mcf/sign/"+id, body)
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("want 422, got %d", code)
		}
	})

	t.Run("invalid id path -> 422", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{}), http.MethodPost, "/mcf/sign/not-a-uuid", completeBody("passed"))
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("want 422, got %d", code)
		}
	})
}

func TestMCFHandlerGetListDelete(t *testing.T) {
	id := mcfUUID()
	hid := mcfUUID()

	t.Run("get by id", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{byID: openMCF()}), http.MethodGet, "/mcf/get/"+id, "")
		if code != http.StatusOK {
			t.Fatalf("want 200, got %d", code)
		}
	})
	t.Run("get not found", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{}), http.MethodGet, "/mcf/get/"+id, "")
		if code != http.StatusNotFound {
			t.Fatalf("want 404, got %d", code)
		}
	})
	t.Run("get invalid uuid", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{}), http.MethodGet, "/mcf/get/bad", "")
		if code != http.StatusUnprocessableEntity {
			t.Fatalf("want 422, got %d", code)
		}
	})
	t.Run("list by helicopter", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{latest: openMCF()}), http.MethodGet, "/mcf/get-by-helicopter/"+hid, "")
		if code != http.StatusOK {
			t.Fatalf("want 200, got %d", code)
		}
	})
	t.Run("list error", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{listErr: context.DeadlineExceeded}), http.MethodGet, "/mcf/get-by-helicopter/"+hid, "")
		if code != http.StatusBadRequest {
			t.Fatalf("want 400, got %d", code)
		}
	})
}

func TestMCFHandlerErrorAndHistoryBranches(t *testing.T) {
	t.Run("activate service error -> 400", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{createErr: context.DeadlineExceeded}), http.MethodPost, "/mcf/set/"+mcfUUID(),
			`{"data":{"type":"mcf_set","attributes":{"active":true}}}`)
		if code != http.StatusBadRequest {
			t.Fatalf("want 400, got %d", code)
		}
	})

	t.Run("activate not-found maps to 404", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{createErr: mcf.ErrNotFound}), http.MethodPost, "/mcf/set/"+mcfUUID(),
			`{"data":{"type":"mcf_set","attributes":{"active":true}}}`)
		if code != http.StatusNotFound {
			t.Fatalf("want 404, got %d", code)
		}
	})

	t.Run("sign update error -> 400", func(t *testing.T) {
		code, _ := doMCF(t, newMCFApp(&stubMCFSvc{byID: openMCF(), updateErr: context.DeadlineExceeded}), http.MethodPost, "/mcf/sign/"+mcfUUID(), completeBody("passed"))
		if code != http.StatusBadRequest {
			t.Fatalf("want 400, got %d", code)
		}
	})

	t.Run("sign with notes", func(t *testing.T) {
		svc := &stubMCFSvc{byID: openMCF()}
		body := `{"data":{"type":"mcf","attributes":{"af_hours":"1","landing_count":2,"utc_time":"2026-06-17T08:30:00Z","date":"2026-06-17","result":"passed","notes":"vibration ok"}}}`
		code, _ := doMCF(t, newMCFApp(svc), http.MethodPost, "/mcf/sign/"+mcfUUID(), body)
		if code != http.StatusOK || svc.updated == nil || svc.updated.Notes == nil || *svc.updated.Notes != "vibration ok" {
			t.Fatalf("want 200 with notes recorded, got %d", code)
		}
	})

	t.Run("sign records history", func(t *testing.T) {
		svc := &stubMCFSvc{byID: openMCF()}
		hist := &stubHistoryRec{}
		h := NewMCFHandler(svc).WithHistoryService(hist)
		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error { c.Locals("user_id", []byte("actor0123456789a")); return c.Next() })
		app.Post("/mcf/sign/:id", h.Sign)
		code, _ := doMCF(t, app, http.MethodPost, "/mcf/sign/"+mcfUUID(), completeBody("failed"))
		if code != http.StatusOK {
			t.Fatalf("want 200, got %d", code)
		}
		if hist.n != 1 {
			t.Fatalf("expected 1 history record, got %d", hist.n)
		}
	})
}
