package service

import (
	"context"
	"strings"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	"github.com/go-playground/validator/v10"
	commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
	rTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
	rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
	rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
	"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
	"gorm.io/gorm"
)

func TestCreateSystemTransferCreatesAuditableMovement(t *testing.T) {
	db := setupSystemTransferTestDB(t)
	svc, fifoStub := newSystemTransferTestService(t, db)

	transferDate := time.Date(2026, 4, 7, 0, 0, 0, 0, time.UTC)
	result, err := svc.CreateSystemTransfer(context.Background(), &SystemTransferRequest{
		TransferReason:         "EGG_FARM_CUTOVER|run_id=test-1|location=Jamali|cutover_date=2026-04-07",
		TransferDate:           transferDate,
		SourceWarehouseID:      1,
		DestinationWarehouseID: 2,
		Products: []SystemTransferProduct{
			{ProductID: 8, ProductQty: 50},
		},
		ActorID:        99,
		MovementNumber: "PND-LTI-TEST-0001",
		StockLogNotes:  "EGG_FARM_CUTOVER|run_id=test-1|location=Jamali|cutover_date=2026-04-07",
	})
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if result == nil {
		t.Fatal("expected transfer result")
	}
	if result.MovementNumber != "PND-LTI-TEST-0001" {
		t.Fatalf("expected movement number to be preserved, got %s", result.MovementNumber)
	}

	var transfer entity.StockTransfer
	if err := db.WithContext(context.Background()).First(&transfer, result.Id).Error; err != nil {
		t.Fatalf("failed to load created transfer: %v", err)
	}

	var detail entity.StockTransferDetail
	if err := db.WithContext(context.Background()).
		Where("stock_transfer_id = ?", transfer.Id).
		First(&detail).Error; err != nil {
		t.Fatalf("failed to load transfer detail: %v", err)
	}
	if detail.UsageQty != 50 {
		t.Fatalf("expected usage qty 50, got %v", detail.UsageQty)
	}
	if detail.TotalQty != 50 {
		t.Fatalf("expected total qty 50, got %v", detail.TotalQty)
	}
	if detail.SourceProductWarehouseID == nil || *detail.SourceProductWarehouseID != 10 {
		t.Fatalf("expected source product warehouse 10, got %+v", detail.SourceProductWarehouseID)
	}
	if detail.DestProductWarehouseID == nil {
		t.Fatal("expected destination product warehouse to be created")
	}

	var destPW entity.ProductWarehouse
	if err := db.WithContext(context.Background()).
		First(&destPW, *detail.DestProductWarehouseID).Error; err != nil {
		t.Fatalf("failed to load destination product warehouse: %v", err)
	}
	if destPW.WarehouseId != 2 {
		t.Fatalf("expected destination warehouse id 2, got %d", destPW.WarehouseId)
	}
	if destPW.ProductId != 8 {
		t.Fatalf("expected destination product id 8, got %d", destPW.ProductId)
	}
	if destPW.ProjectFlockKandangId != nil {
		t.Fatalf("expected destination product warehouse to stay shared, got %+v", destPW.ProjectFlockKandangId)
	}

	var stockLogs []entity.StockLog
	if err := db.WithContext(context.Background()).
		Order("id ASC").
		Find(&stockLogs).Error; err != nil {
		t.Fatalf("failed to load stock logs: %v", err)
	}
	if len(stockLogs) != 3 {
		t.Fatalf("expected 3 stock logs (seed + out + in), got %d", len(stockLogs))
	}
	if stockLogs[1].ProductWarehouseId != 10 || stockLogs[1].Decrease != 50 || stockLogs[1].Stock != 0 {
		t.Fatalf("unexpected source stock log after transfer: %+v", stockLogs[1])
	}
	if stockLogs[2].ProductWarehouseId != destPW.Id || stockLogs[2].Increase != 50 || stockLogs[2].Stock != 50 {
		t.Fatalf("unexpected destination stock log after transfer: %+v", stockLogs[2])
	}

	if len(fifoStub.reflowCalls) != 2 {
		t.Fatalf("expected 2 reflow calls, got %d", len(fifoStub.reflowCalls))
	}
	if fifoStub.reflowCalls[0].ProductWarehouseID != 10 {
		t.Fatalf("expected first reflow on source pw 10, got %d", fifoStub.reflowCalls[0].ProductWarehouseID)
	}
	if fifoStub.reflowCalls[1].ProductWarehouseID != destPW.Id {
		t.Fatalf("expected second reflow on destination pw %d, got %d", destPW.Id, fifoStub.reflowCalls[1].ProductWarehouseID)
	}
}

func TestDeleteSystemTransferRollsBackTransferWhenUnused(t *testing.T) {
	db := setupSystemTransferTestDB(t)
	svc, fifoStub := newSystemTransferTestService(t, db)

	created, err := svc.CreateSystemTransfer(context.Background(), &SystemTransferRequest{
		TransferReason:         "EGG_FARM_CUTOVER|run_id=test-rollback|location=Jamali|cutover_date=2026-04-07",
		TransferDate:           time.Date(2026, 4, 7, 0, 0, 0, 0, time.UTC),
		SourceWarehouseID:      1,
		DestinationWarehouseID: 2,
		Products:               []SystemTransferProduct{{ProductID: 8, ProductQty: 50}},
		ActorID:                99,
		MovementNumber:         "PND-LTI-TEST-ROLLBACK",
		StockLogNotes:          "EGG_FARM_CUTOVER|run_id=test-rollback|location=Jamali|cutover_date=2026-04-07",
	})
	if err != nil {
		t.Fatalf("failed to create transfer: %v", err)
	}

	var detail entity.StockTransferDetail
	if err := db.WithContext(context.Background()).
		Where("stock_transfer_id = ?", created.Id).
		First(&detail).Error; err != nil {
		t.Fatalf("failed to load transfer detail: %v", err)
	}
	fifoStub.rollbackReleasedQty[detail.Id] = detail.UsageQty

	if err := svc.DeleteSystemTransfer(context.Background(), uint(created.Id), 99); err != nil {
		t.Fatalf("expected delete to succeed, got %v", err)
	}

	var deletedTransfer entity.StockTransfer
	if err := db.WithContext(context.Background()).Unscoped().First(&deletedTransfer, created.Id).Error; err != nil {
		t.Fatalf("failed to load deleted transfer: %v", err)
	}
	if deletedTransfer.DeletedAt == nil {
		t.Fatal("expected transfer to be soft deleted")
	}

	var deletedDetail entity.StockTransferDetail
	if err := db.WithContext(context.Background()).Unscoped().First(&deletedDetail, detail.Id).Error; err != nil {
		t.Fatalf("failed to load deleted transfer detail: %v", err)
	}
	if deletedDetail.DeletedAt == nil {
		t.Fatal("expected transfer detail to be soft deleted")
	}

	var stockLogs []entity.StockLog
	if err := db.WithContext(context.Background()).
		Order("id ASC").
		Find(&stockLogs).Error; err != nil {
		t.Fatalf("failed to load stock logs: %v", err)
	}
	if len(stockLogs) != 5 {
		t.Fatalf("expected 5 stock logs (seed + create out/in + delete in/out), got %d", len(stockLogs))
	}
	if stockLogs[3].ProductWarehouseId != 10 || stockLogs[3].Increase != 50 || stockLogs[3].Stock != 50 {
		t.Fatalf("unexpected rollback source stock log: %+v", stockLogs[3])
	}
	if stockLogs[4].Decrease != 50 || stockLogs[4].Stock != 0 {
		t.Fatalf("unexpected rollback destination stock log: %+v", stockLogs[4])
	}
	if len(fifoStub.rollbackCalls) != 1 {
		t.Fatalf("expected 1 rollback call, got %d", len(fifoStub.rollbackCalls))
	}
	if len(fifoStub.reflowCalls) != 3 {
		t.Fatalf("expected 3 reflow calls (2 create + 1 delete), got %d", len(fifoStub.reflowCalls))
	}
}

func TestDeleteSystemTransferRejectsRollbackWhenDownstreamConsumptionExists(t *testing.T) {
	db := setupSystemTransferTestDB(t)
	svc, fifoStub := newSystemTransferTestService(t, db)

	created, err := svc.CreateSystemTransfer(context.Background(), &SystemTransferRequest{
		TransferReason:         "EGG_FARM_CUTOVER|run_id=test-guard|location=Jamali|cutover_date=2026-04-07",
		TransferDate:           time.Date(2026, 4, 7, 0, 0, 0, 0, time.UTC),
		SourceWarehouseID:      1,
		DestinationWarehouseID: 2,
		Products:               []SystemTransferProduct{{ProductID: 8, ProductQty: 50}},
		ActorID:                99,
		MovementNumber:         "PND-LTI-TEST-GUARD",
		StockLogNotes:          "EGG_FARM_CUTOVER|run_id=test-guard|location=Jamali|cutover_date=2026-04-07",
	})
	if err != nil {
		t.Fatalf("failed to create transfer: %v", err)
	}

	var detail entity.StockTransferDetail
	if err := db.WithContext(context.Background()).
		Where("stock_transfer_id = ?", created.Id).
		First(&detail).Error; err != nil {
		t.Fatalf("failed to load transfer detail: %v", err)
	}

	if err := db.Exec(`
		INSERT INTO stock_allocations (
			id, product_warehouse_id, stockable_type, stockable_id, usable_type, usable_id, qty,
			allocation_purpose, status, function_code, flag_group_code, deleted_at
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
	`, 1, *detail.DestProductWarehouseID, fifo.StockableKeyStockTransferIn.String(), detail.Id, fifo.UsableKeyRecordingStock.String(), 9001, 10,
		entity.StockAllocationPurposeConsume, entity.StockAllocationStatusActive, "RECORDING_STOCK_OUT", "EGG").Error; err != nil {
		t.Fatalf("failed to seed stock allocation: %v", err)
	}

	err = svc.DeleteSystemTransfer(context.Background(), uint(created.Id), 99)
	if err == nil {
		t.Fatal("expected delete to be blocked by downstream consumption")
	}
	if !strings.Contains(err.Error(), "tidak dapat dihapus") {
		t.Fatalf("expected downstream guard error, got %v", err)
	}
	if len(fifoStub.rollbackCalls) != 0 {
		t.Fatalf("expected rollback not to be called, got %d calls", len(fifoStub.rollbackCalls))
	}

	var transfer entity.StockTransfer
	if err := db.WithContext(context.Background()).First(&transfer, created.Id).Error; err != nil {
		t.Fatalf("failed to reload transfer: %v", err)
	}
	if transfer.DeletedAt != nil {
		t.Fatal("expected transfer to remain active after guard failure")
	}
}

type fifoStockV2Stub struct {
	reflowCalls         []commonSvc.FifoStockV2ReflowRequest
	rollbackCalls       []commonSvc.FifoStockV2RollbackRequest
	rollbackReleasedQty map[uint64]float64
}

func (f *fifoStockV2Stub) Gather(ctx context.Context, req commonSvc.FifoStockV2GatherRequest) ([]commonSvc.FifoStockV2GatherRow, error) {
	return nil, nil
}

func (f *fifoStockV2Stub) Allocate(ctx context.Context, req commonSvc.FifoStockV2AllocateRequest) (*commonSvc.FifoStockV2AllocateResult, error) {
	return nil, nil
}

func (f *fifoStockV2Stub) Rollback(ctx context.Context, req commonSvc.FifoStockV2RollbackRequest) (*commonSvc.FifoStockV2RollbackResult, error) {
	f.rollbackCalls = append(f.rollbackCalls, req)
	return &commonSvc.FifoStockV2RollbackResult{
		ReleasedQty: f.rollbackReleasedQty[uint64(req.Usable.ID)],
	}, nil
}

func (f *fifoStockV2Stub) Reflow(ctx context.Context, req commonSvc.FifoStockV2ReflowRequest) (*commonSvc.FifoStockV2ReflowResult, error) {
	f.reflowCalls = append(f.reflowCalls, req)
	return &commonSvc.FifoStockV2ReflowResult{}, nil
}

func (f *fifoStockV2Stub) Recalculate(ctx context.Context, req commonSvc.FifoStockV2RecalculateRequest) (*commonSvc.FifoStockV2RecalculateResult, error) {
	return &commonSvc.FifoStockV2RecalculateResult{}, nil
}

func newSystemTransferTestService(t *testing.T, db *gorm.DB) (TransferService, *fifoStockV2Stub) {
	t.Helper()

	fifoStub := &fifoStockV2Stub{rollbackReleasedQty: make(map[uint64]float64)}
	return NewTransferService(
		validator.New(),
		rTransfer.NewStockTransferRepository(db),
		rTransfer.NewStockTransferDetailRepository(db),
		rTransfer.NewStockTransferDeliveryRepository(db),
		rTransfer.NewStockTransferDeliveryItemRepository(db),
		rStockLogs.NewStockLogRepository(db),
		rProductWarehouse.NewProductWarehouseRepository(db),
		nil,
		rWarehouse.NewWarehouseRepository(db),
		nil,
		nil,
		nil,
		fifoStub,
		nil,
	), fifoStub
}

func setupSystemTransferTestDB(t *testing.T) *gorm.DB {
	t.Helper()

	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	if err != nil {
		t.Fatalf("failed opening sqlite db: %v", err)
	}

	statements := []string{
		`CREATE TABLE warehouses (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			name TEXT NOT NULL,
			type TEXT NOT NULL,
			area_id INTEGER NOT NULL DEFAULT 1,
			location_id INTEGER NULL,
			kandang_id INTEGER NULL,
			created_by INTEGER NOT NULL DEFAULT 1,
			created_at TIMESTAMP NULL,
			updated_at TIMESTAMP NULL,
			deleted_at TIMESTAMP NULL
		)`,
		`CREATE TABLE product_categories (
			id INTEGER PRIMARY KEY,
			name TEXT NULL,
			code TEXT NOT NULL,
			created_by INTEGER NOT NULL DEFAULT 1,
			created_at TIMESTAMP NULL,
			updated_at TIMESTAMP NULL,
			deleted_at TIMESTAMP NULL
		)`,
		`CREATE TABLE products (
			id INTEGER PRIMARY KEY,
			name TEXT NOT NULL,
			brand TEXT NOT NULL DEFAULT '',
			sku TEXT NULL,
			uom_id INTEGER NOT NULL DEFAULT 1,
			product_category_id INTEGER NULL,
			product_price NUMERIC NOT NULL DEFAULT 0,
			selling_price NUMERIC NULL,
			tax NUMERIC NULL,
			expiry_period INTEGER NULL,
			created_by INTEGER NOT NULL DEFAULT 1,
			created_at TIMESTAMP NULL,
			updated_at TIMESTAMP NULL,
			deleted_at TIMESTAMP NULL,
			is_visible BOOLEAN NOT NULL DEFAULT 1
		)`,
		`CREATE TABLE product_warehouses (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			product_id INTEGER NOT NULL,
			warehouse_id INTEGER NOT NULL,
			project_flock_kandang_id INTEGER NULL,
			qty NUMERIC NOT NULL DEFAULT 0
		)`,
		`CREATE TABLE flags (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			name TEXT NOT NULL,
			flagable_id INTEGER NOT NULL,
			flagable_type TEXT NOT NULL
		)`,
		`CREATE TABLE fifo_stock_v2_flag_groups (
			code TEXT PRIMARY KEY,
			is_active BOOLEAN NOT NULL
		)`,
		`CREATE TABLE fifo_stock_v2_flag_members (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			flag_name TEXT NOT NULL,
			flag_group_code TEXT NOT NULL,
			is_active BOOLEAN NOT NULL
		)`,
		`CREATE TABLE fifo_stock_v2_route_rules (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			lane TEXT NOT NULL,
			function_code TEXT NOT NULL,
			source_table TEXT NOT NULL,
			flag_group_code TEXT NOT NULL,
			legacy_type_key TEXT NULL,
			allow_pending_default BOOLEAN NOT NULL DEFAULT 0,
			is_active BOOLEAN NOT NULL
		)`,
		`CREATE TABLE fifo_stock_v2_overconsume_rules (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			lane TEXT NOT NULL,
			flag_group_code TEXT NULL,
			function_code TEXT NULL,
			allow_overconsume BOOLEAN NOT NULL DEFAULT 0,
			is_active BOOLEAN NOT NULL DEFAULT 1,
			priority INTEGER NOT NULL DEFAULT 1
		)`,
		`CREATE TABLE stock_transfers (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			movement_number TEXT NOT NULL,
			from_warehouse_id INTEGER NOT NULL,
			to_warehouse_id INTEGER NOT NULL,
			transfer_date TIMESTAMP NOT NULL,
			reason TEXT,
			created_by INTEGER NOT NULL,
			created_at TIMESTAMP NULL,
			updated_at TIMESTAMP NULL,
			deleted_at TIMESTAMP NULL
		)`,
		`CREATE TABLE stock_transfer_details (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			stock_transfer_id INTEGER NOT NULL,
			product_id INTEGER NOT NULL,
			source_product_warehouse_id INTEGER NULL,
			usage_qty NUMERIC NOT NULL DEFAULT 0,
			pending_qty NUMERIC NOT NULL DEFAULT 0,
			dest_product_warehouse_id INTEGER NULL,
			total_qty NUMERIC NOT NULL DEFAULT 0,
			total_used NUMERIC NOT NULL DEFAULT 0,
			expense_nonstock_id INTEGER NULL,
			created_at TIMESTAMP NULL,
			updated_at TIMESTAMP NULL,
			deleted_at TIMESTAMP NULL
		)`,
		`CREATE TABLE stock_transfer_deliveries (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			stock_transfer_id INTEGER NOT NULL,
			supplier_id INTEGER NULL,
			vehicle_plate TEXT NULL,
			driver_name TEXT NULL,
			shipping_cost_item NUMERIC NULL,
			shipping_cost_total NUMERIC NULL,
			created_at TIMESTAMP NULL,
			updated_at TIMESTAMP NULL,
			deleted_at TIMESTAMP NULL
		)`,
		`CREATE TABLE stock_transfer_delivery_items (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			stock_transfer_delivery_id INTEGER NOT NULL,
			stock_transfer_detail_id INTEGER NOT NULL,
			quantity NUMERIC NOT NULL DEFAULT 0,
			created_at TIMESTAMP NULL,
			updated_at TIMESTAMP NULL,
			deleted_at TIMESTAMP NULL
		)`,
		`CREATE TABLE stock_logs (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			product_warehouse_id INTEGER NOT NULL,
			created_by INTEGER NOT NULL,
			increase NUMERIC NOT NULL DEFAULT 0,
			decrease NUMERIC NOT NULL DEFAULT 0,
			stock NUMERIC NOT NULL DEFAULT 0,
			loggable_type TEXT NOT NULL,
			loggable_id INTEGER NOT NULL,
			notes TEXT NULL,
			created_at TIMESTAMP NULL
		)`,
		`CREATE TABLE stock_allocations (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			product_warehouse_id INTEGER NOT NULL,
			stockable_type TEXT NOT NULL,
			stockable_id INTEGER NOT NULL,
			usable_type TEXT NOT NULL,
			usable_id INTEGER NOT NULL,
			qty NUMERIC NOT NULL DEFAULT 0,
			allocation_purpose TEXT NOT NULL,
			status TEXT NOT NULL,
			function_code TEXT NULL,
			flag_group_code TEXT NULL,
			deleted_at TIMESTAMP NULL
		)`,
		`INSERT INTO warehouses (id, name, type, area_id, location_id, kandang_id, created_by, created_at, updated_at, deleted_at) VALUES
			(1, 'Gudang Kandang Legacy', 'LOKASI', 1, 16, NULL, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL),
			(2, 'Gudang Farm Jamali', 'LOKASI', 1, 16, NULL, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL)`,
		`INSERT INTO product_categories (id, name, code, created_by, created_at, updated_at, deleted_at) VALUES (1, 'Egg', 'EGG', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL)`,
		`INSERT INTO products (
			id, name, brand, sku, uom_id, product_category_id, product_price, selling_price, tax,
			expiry_period, created_by, created_at, updated_at, deleted_at, is_visible
		) VALUES (
			8, 'Telur Utuh', '', NULL, 1, 1, 0, NULL, NULL, NULL, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, 1
		)`,
		`INSERT INTO product_warehouses (id, product_id, warehouse_id, project_flock_kandang_id, qty) VALUES
			(10, 8, 1, NULL, 50)`,
		`INSERT INTO flags (name, flagable_id, flagable_type) VALUES ('TELUR', 8, 'products')`,
		`INSERT INTO fifo_stock_v2_flag_groups (code, is_active) VALUES ('EGG', 1)`,
		`INSERT INTO fifo_stock_v2_flag_members (flag_name, flag_group_code, is_active) VALUES ('TELUR', 'EGG', 1)`,
		`INSERT INTO fifo_stock_v2_route_rules (lane, function_code, source_table, flag_group_code, legacy_type_key, allow_pending_default, is_active) VALUES
			('USABLE', 'STOCK_TRANSFER_OUT', 'stock_transfer_details', 'EGG', 'STOCK_TRANSFER_OUT', 0, 1)`,
		`INSERT INTO stock_logs (id, product_warehouse_id, created_by, increase, decrease, stock, loggable_type, loggable_id, notes, created_at) VALUES
			(1, 10, 1, 50, 0, 50, 'PURCHASE', 1, 'seed', CURRENT_TIMESTAMP)`,
	}

	for _, stmt := range statements {
		if err := db.Exec(stmt).Error; err != nil {
			t.Fatalf("failed preparing test schema: %v\nstatement: %s", err, stmt)
		}
	}

	return db
}
