package test

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"testing"

	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"

	"gitlab.com/mbugroup/lti-api.git/internal/entities"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
)

func TestNonstockIntegration(t *testing.T) {
	app, db := setupIntegrationApp(t)

	uomID := createUom(t, app, "Unit Piece")
	altUomID := createUom(t, app, "Unit Box")

	supplierID1 := createSupplier(t, app, "Nonstock Supplier One", "ns1", string(utils.SupplierCategoryBOP))
	supplierID2 := createSupplier(t, app, "Nonstock Supplier Two", "ns2", string(utils.SupplierCategoryBOP))
	supplierID3 := createSupplier(t, app, "Nonstock Supplier Three", "ns3", string(utils.SupplierCategoryBOP))
	sapronakSupplierID := createSupplier(t, app, "SAPRONAK Supplier", "sap1", string(utils.SupplierCategorySapronak))

	nonstockFlags := []string{string(utils.FlagEkspedisi)}

	t.Run("create nonstock without suppliers succeeds with empty relations", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{
			"name":   "Supplierless Nonstock",
			"uom_id": uomID,
		})
		if resp.StatusCode != fiber.StatusCreated {
			t.Fatalf("expected 201 when suppliers omitted, got %d: %s", resp.StatusCode, string(body))
		}
		id := parseID(t, body)
		ns := fetchNonstock(t, db, id)
		if len(ns.Suppliers) != 0 {
			t.Fatalf("expected no suppliers persisted, found %d", len(ns.Suppliers))
		}
		if len(ns.Flags) != 0 {
			t.Fatalf("expected no flags persisted, found %d", len(ns.Flags))
		}
		resp, _ = doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", id), nil)
		if resp.StatusCode != fiber.StatusOK {
			t.Fatalf("expected cleanup delete to succeed, got %d", resp.StatusCode)
		}
	})

	t.Run("create nonstock with unknown supplier fails", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{
			"name":         "Unknown Supplier Nonstock",
			"uom_id":       uomID,
			"supplier_ids": []uint{99999},
		})
		if resp.StatusCode != fiber.StatusNotFound {
			t.Fatalf("expected 404 when supplier missing, got %d: %s", resp.StatusCode, string(body))
		}
	})

	t.Run("create nonstock with sapronak supplier fails", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{
			"name":         "Invalid Category Nonstock",
			"uom_id":       uomID,
			"supplier_ids": []uint{sapronakSupplierID},
		})
		if resp.StatusCode != fiber.StatusBadRequest {
			t.Fatalf("expected 400 when supplier category is not BOP, got %d: %s", resp.StatusCode, string(body))
		}
	})

	t.Run("create nonstock with invalid flags fails", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodPost, "/api/master-data/nonstocks", map[string]any{
			"name":         "Invalid Flag Nonstock",
			"uom_id":       uomID,
			"supplier_ids": []uint{supplierID1},
			"flags":        []string{"UNKNOWN"},
		})
		if resp.StatusCode != fiber.StatusBadRequest {
			t.Fatalf("expected 400 when flags invalid, got %d: %s", resp.StatusCode, string(body))
		}
	})

	var nonstockID uint

	t.Run("create nonstock succeeds", func(t *testing.T) {
		nonstockID = createNonstock(t, app, "Layer Feed", uomID, []uint{supplierID1, supplierID2, supplierID1}, nonstockFlags)
		if nonstockID == 0 {
			t.Fatal("expected nonstock id to be non zero")
		}
		ns := fetchNonstock(t, db, nonstockID)
		if ns.Name != "Layer Feed" {
			t.Fatalf("expected name Layer Feed, got %q", ns.Name)
		}
		if ns.UomId != uomID {
			t.Fatalf("expected uom_id %d, got %d", uomID, ns.UomId)
		}
		if ns.CreatedBy != 1 {
			t.Fatalf("expected created_by 1, got %d", ns.CreatedBy)
		}
		if len(ns.Suppliers) != 2 {
			t.Fatalf("expected 2 unique suppliers, got %d", len(ns.Suppliers))
		}
		if len(ns.Flags) != len(nonstockFlags) {
			t.Fatalf("expected %d flags, got %d", len(nonstockFlags), len(ns.Flags))
		}
		expectedFlags := make(map[string]struct{}, len(nonstockFlags))
		for _, flag := range nonstockFlags {
			expectedFlags[strings.ToUpper(flag)] = struct{}{}
		}
		for _, flag := range ns.Flags {
			upper := strings.ToUpper(flag.Name)
			if _, ok := expectedFlags[upper]; !ok {
				t.Fatalf("unexpected flag stored: %s", upper)
			}
			delete(expectedFlags, upper)
		}
		if len(expectedFlags) != 0 {
			t.Fatalf("missing flags after create: %v", expectedFlags)
		}
	})

	t.Run("get nonstock detail includes suppliers", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil)
		if resp.StatusCode != fiber.StatusOK {
			t.Fatalf("expected 200 when fetching nonstock, got %d: %s", resp.StatusCode, string(body))
		}
		var payload struct {
			Data struct {
				Id        uint             `json:"id"`
				Name      string           `json:"name"`
				UomID     uint             `json:"uom_id"`
				Suppliers []map[string]any `json:"suppliers"`
				Flags     []string         `json:"flags"`
			} `json:"data"`
		}
		if err := json.Unmarshal(body, &payload); err != nil {
			t.Fatalf("failed to parse nonstock detail: %v", err)
		}
		if payload.Data.Id != nonstockID {
			t.Fatalf("expected id %d, got %d", nonstockID, payload.Data.Id)
		}
		if payload.Data.UomID != uomID {
			t.Fatalf("expected response uom_id %d, got %d", uomID, payload.Data.UomID)
		}
		if len(payload.Data.Suppliers) != 2 {
			t.Fatalf("expected 2 suppliers in response, got %d", len(payload.Data.Suppliers))
		}
		if len(payload.Data.Flags) != len(nonstockFlags) {
			t.Fatalf("expected %d flags in response, got %d", len(nonstockFlags), len(payload.Data.Flags))
		}
		expected := make(map[string]struct{}, len(nonstockFlags))
		for _, flag := range nonstockFlags {
			expected[strings.ToUpper(flag)] = struct{}{}
		}
		for _, flag := range payload.Data.Flags {
			flag = strings.ToUpper(flag)
			if _, ok := expected[flag]; !ok {
				t.Fatalf("unexpected flag %s returned", flag)
			}
			delete(expected, flag)
		}
		if len(expected) != 0 {
			t.Fatalf("missing flags in response: %v", expected)
		}
	})

	t.Run("update nonstock with invalid uom fails", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
			"uom_id": 99999,
		})
		if resp.StatusCode != fiber.StatusNotFound {
			t.Fatalf("expected 404 when updating with invalid uom, got %d: %s", resp.StatusCode, string(body))
		}
	})

	t.Run("update nonstock with invalid supplier fails", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
			"supplier_ids": []uint{supplierID1, 99999},
		})
		if resp.StatusCode != fiber.StatusNotFound {
			t.Fatalf("expected 404 when updating with invalid supplier, got %d: %s", resp.StatusCode, string(body))
		}
	})

	t.Run("update nonstock with sapronak supplier fails", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
			"supplier_ids": []uint{sapronakSupplierID},
		})
		if resp.StatusCode != fiber.StatusBadRequest {
			t.Fatalf("expected 400 when updating with non-BOP supplier, got %d: %s", resp.StatusCode, string(body))
		}
	})

	t.Run("update nonstock with invalid flags fails", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
			"flags": []string{"BAD"},
		})
		if resp.StatusCode != fiber.StatusBadRequest {
			t.Fatalf("expected 400 when updating flags invalid, got %d: %s", resp.StatusCode, string(body))
		}
	})

	t.Run("update nonstock name uom and suppliers succeeds", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
			"name":         "Layer Feed Premium",
			"uom_id":       altUomID,
			"supplier_ids": []uint{supplierID3},
			"flags":        nonstockFlags,
		})
		if resp.StatusCode != fiber.StatusOK {
			t.Fatalf("expected 200 when updating nonstock, got %d: %s", resp.StatusCode, string(body))
		}
		ns := fetchNonstock(t, db, nonstockID)
		if ns.Name != "Layer Feed Premium" {
			t.Fatalf("expected name Layer Feed Premium, got %q", ns.Name)
		}
		if ns.UomId != altUomID {
			t.Fatalf("expected uom_id %d, got %d", altUomID, ns.UomId)
		}
		if len(ns.Suppliers) != 1 || ns.Suppliers[0].Id != supplierID3 {
			t.Fatalf("expected suppliers to contain only %d", supplierID3)
		}
		if len(ns.Flags) != len(nonstockFlags) {
			t.Fatalf("expected flags retained, got %d", len(ns.Flags))
		}
	})

	t.Run("clear suppliers succeeds", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
			"supplier_ids": []uint{},
		})
		if resp.StatusCode != fiber.StatusOK {
			t.Fatalf("expected 200 when clearing suppliers, got %d: %s", resp.StatusCode, string(body))
		}
		ns := fetchNonstock(t, db, nonstockID)
		if len(ns.Suppliers) != 0 {
			t.Fatalf("expected suppliers to be cleared, got %d entries", len(ns.Suppliers))
		}
		if len(ns.Flags) != len(nonstockFlags) {
			t.Fatalf("expected flags unaffected, got %d", len(ns.Flags))
		}
	})

	t.Run("clear nonstock flags succeeds", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodPatch, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), map[string]any{
			"flags": []string{},
		})
		if resp.StatusCode != fiber.StatusOK {
			t.Fatalf("expected 200 when clearing nonstock flags, got %d: %s", resp.StatusCode, string(body))
		}
		ns := fetchNonstock(t, db, nonstockID)
		if len(ns.Flags) != 0 {
			t.Fatalf("expected flags cleared, got %d", len(ns.Flags))
		}
	})

	t.Run("delete nonstock succeeds", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil)
		if resp.StatusCode != fiber.StatusOK {
			t.Fatalf("expected 200 when deleting nonstock, got %d: %s", resp.StatusCode, string(body))
		}
		var ns entities.Nonstock
		if err := db.First(&ns, nonstockID).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
			t.Fatalf("expected nonstock to be deleted, got error %v", err)
		}
		var links int64
		if err := db.Model(&entities.NonstockSupplier{}).Where("nonstock_id = ?", nonstockID).Count(&links).Error; err != nil {
			t.Fatalf("failed counting nonstock suppliers: %v", err)
		}
		if links != 0 {
			t.Fatalf("expected link table cleared, found %d rows", links)
		}
		var flagCount int64
		if err := db.Model(&entities.Flag{}).
			Where("flagable_id = ? AND flagable_type = ?", nonstockID, entities.FlagableTypeNonstock).
			Count(&flagCount).Error; err != nil {
			t.Fatalf("failed counting nonstock flags: %v", err)
		}
		if flagCount != 0 {
			t.Fatalf("expected flags removed, found %d", flagCount)
		}
	})

	t.Run("deleting nonstock twice returns 404", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil)
		if resp.StatusCode != fiber.StatusNotFound {
			t.Fatalf("expected 404 when deleting missing nonstock, got %d: %s", resp.StatusCode, string(body))
		}
	})

	t.Run("fetching deleted nonstock returns 404", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodGet, fmt.Sprintf("/api/master-data/nonstocks/%d", nonstockID), nil)
		if resp.StatusCode != fiber.StatusNotFound {
			t.Fatalf("expected 404 when fetching deleted nonstock, got %d: %s", resp.StatusCode, string(body))
		}
	})

	t.Run("cleanup additional supplier references", func(t *testing.T) {
		resp, body := doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", supplierID3), nil)
		if resp.StatusCode != fiber.StatusOK {
			t.Fatalf("expected 200 when deleting supplier, got %d: %s", resp.StatusCode, string(body))
		}
		doJSONRequest(t, app, http.MethodDelete, fmt.Sprintf("/api/master-data/suppliers/%d", sapronakSupplierID), nil)
	})
}
