package repository

import (
	"context"
	"fmt"
	"math"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
	"gorm.io/gorm"
)

func TestHppV2RepositoryGetEggProduksiIncludesTransferredAdjustmentStock(t *testing.T) {
	db := setupHppV2RepositoryTestDB(t)

	mustExecHppV2(t, db,
		`INSERT INTO recordings (id, project_flock_kandangs_id, record_datetime) VALUES (1, 101, '2026-04-19 10:00:00')`,
		`INSERT INTO recording_eggs (id, recording_id, product_warehouse_id, qty, weight, project_flock_kandang_id) VALUES (1, 1, 401, 80, 8, 101)`,
		`INSERT INTO stock_transfers (id, transfer_date) VALUES (1, '2026-04-18 08:00:00')`,
		`INSERT INTO stock_transfer_details (id, stock_transfer_id, source_product_warehouse_id, dest_product_warehouse_id) VALUES (1, 1, 301, 401)`,
		`INSERT INTO stock_allocations (id, usable_type, usable_id, stockable_type, stockable_id, status, allocation_purpose) VALUES (1, 'STOCK_TRANSFER_OUT', 1, 'ADJUSTMENT_IN', 501, 'ACTIVE', 'CONSUME')`,
		`INSERT INTO adjustment_stocks (id, product_warehouse_id, total_qty, price, created_at) VALUES (501, 301, 20, 2.5, '2026-04-18 07:30:00')`,
	)

	repo := &HppV2RepositoryImpl{db: db}
	endDate := mustJakartaTime(t, "2026-04-20 00:00:00")

	totalPieces, totalWeightKg, err := repo.GetEggProduksiPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{101}, &endDate)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}

	assertFloatEquals(t, totalPieces, 100)
	assertFloatEquals(t, totalWeightKg, 10.5)
}

func TestHppV2RepositoryGetEggTerjualUsesEndDateForSameDayFarmSales(t *testing.T) {
	db := setupHppV2RepositoryTestDB(t)

	mustExecHppV2(t, db,
		`INSERT INTO kandangs (id, location_id) VALUES (1, 10), (2, 10)`,
		`INSERT INTO project_flock_kandangs (id, kandang_id) VALUES (101, 1), (102, 2)`,
		`INSERT INTO warehouses (id, type, location_id) VALUES (201, 'LOKASI', 10)`,
		`INSERT INTO product_warehouses (id, warehouse_id, product_id, project_flock_kandang_id) VALUES (301, 201, 900, NULL)`,
		`INSERT INTO flags (id, flagable_type, flagable_id, name) VALUES (1, 'products', 900, 'TELUR')`,
		`INSERT INTO recordings (id, project_flock_kandangs_id, record_datetime, deleted_at) VALUES (1, 101, '2026-04-19 08:00:00', NULL), (2, 102, '2026-04-19 09:00:00', NULL)`,
		`INSERT INTO recording_eggs (id, recording_id, product_warehouse_id, qty, weight, project_flock_kandang_id) VALUES (1, 1, 301, 60, 6, 101), (2, 2, 301, 40, 4, 102)`,
		`INSERT INTO marketing_products (id, product_warehouse_id) VALUES (401, 301)`,
		`INSERT INTO marketing_delivery_products (id, marketing_product_id, usage_qty, total_weight, delivery_date) VALUES (501, 401, 50, 5, '2026-04-19 12:00:00')`,
	)

	repo := &HppV2RepositoryImpl{db: db}
	startDate := mustJakartaTime(t, "2026-04-19 00:00:00")
	endDate := mustJakartaTime(t, "2026-04-20 00:00:00")

	totalPieces, totalWeightKg, err := repo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{101}, &startDate, &endDate)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}

	assertFloatEquals(t, totalPieces, 30)
	assertFloatEquals(t, totalWeightKg, 3)
}

func TestHppV2RepositoryGetEggTerjualProratesHistoricalFarmSalesFromAdjustments(t *testing.T) {
	db := setupHppV2RepositoryTestDB(t)

	mustExecHppV2(t, db,
		`INSERT INTO kandangs (id, location_id) VALUES (1, 10), (2, 10)`,
		`INSERT INTO project_flock_kandangs (id, kandang_id) VALUES (101, 1), (102, 2)`,
		`INSERT INTO warehouses (id, type, location_id) VALUES (201, 'LOKASI', 10), (211, 'KANDANG', 10), (212, 'KANDANG', 10)`,
		`INSERT INTO product_warehouses (id, warehouse_id, product_id, project_flock_kandang_id) VALUES (301, 201, 900, NULL), (311, 211, 900, 101), (312, 212, 900, 102)`,
		`INSERT INTO flags (id, flagable_type, flagable_id, name) VALUES (1, 'products', 900, 'TELUR')`,
		`INSERT INTO stock_transfers (id, transfer_date) VALUES (1, '2026-04-18 08:00:00'), (2, '2026-04-18 08:15:00')`,
		`INSERT INTO stock_transfer_details (id, stock_transfer_id, source_product_warehouse_id, dest_product_warehouse_id) VALUES (1, 1, 311, 301), (2, 2, 312, 301)`,
		`INSERT INTO adjustment_stocks (id, product_warehouse_id, usage_qty, price, function_code, transaction_type, created_at) VALUES
			(801, 311, 70, 7, 'RECORDING_EGG_IN', 'RECORDING', '2026-04-18 07:00:00'),
			(802, 312, 30, 3, 'RECORDING_EGG_IN', 'RECORDING', '2026-04-18 07:30:00')`,
		`INSERT INTO marketing_products (id, product_warehouse_id) VALUES (401, 301)`,
		`INSERT INTO marketing_delivery_products (id, marketing_product_id, usage_qty, total_weight, delivery_date) VALUES (501, 401, 20, 2, '2026-04-19 12:00:00')`,
	)

	repo := &HppV2RepositoryImpl{db: db}
	startDate := mustJakartaTime(t, "2026-04-19 00:00:00")
	endDate := mustJakartaTime(t, "2026-04-20 00:00:00")

	totalPieces, totalWeightKg, err := repo.GetEggTerjualPiecesAndWeightKgByProjectFlockKandangIds(context.Background(), []uint{101}, &startDate, &endDate)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}

	assertFloatEquals(t, totalPieces, 14)
	assertFloatEquals(t, totalWeightKg, 1.4)
}

func TestHppV2RepositoryGetRecordingStockRoutingAdjustmentCostByProjectFlockID(t *testing.T) {
	db := setupHppV2RepositoryTestDB(t)
	approvalType := utils.ApprovalWorkflowTransferToLaying.String()

	mustExecHppV2(t, db,
		`INSERT INTO project_flock_kandangs (id, kandang_id, project_flock_id) VALUES
			(101, 1, 1),
			(102, 2, 1),
			(103, 3, 1),
			(104, 4, 1),
			(105, 5, 1),
			(201, 6, 2)`,
		`INSERT INTO recordings (id, project_flock_kandangs_id, record_datetime, deleted_at) VALUES
			(1, 101, '2026-04-10 08:00:00', NULL),
			(2, 101, '2026-04-10 08:05:00', NULL),
			(3, 101, '2026-04-10 08:10:00', NULL),
			(4, 102, '2026-04-10 08:15:00', NULL),
			(5, 102, '2026-04-10 08:20:00', NULL),
			(6, 103, '2026-04-12 08:00:00', NULL),
			(7, 103, '2026-04-12 08:05:00', NULL),
			(8, 104, '2026-04-12 08:10:00', NULL),
			(9, 104, '2026-04-12 08:15:00', NULL),
			(10, 105, '2026-04-12 08:20:00', NULL),
			(11, 105, '2026-04-12 08:25:00', NULL)`,
		`INSERT INTO product_warehouses (id, warehouse_id, product_id, project_flock_kandang_id) VALUES
			(501, 201, 10, NULL),
			(502, 201, 10, NULL),
			(503, 201, 10, NULL),
			(504, 201, 10, NULL),
			(505, 201, 10, NULL),
			(506, 201, 10, NULL),
			(507, 201, 10, NULL),
			(508, 201, 10, NULL),
			(509, 201, 10, NULL),
			(510, 201, 10, NULL),
			(511, 201, 10, NULL)`,
		`INSERT INTO flags (id, flagable_type, flagable_id, name) VALUES
			(10, 'products', 10, 'PAKAN')`,
		`INSERT INTO recording_stocks (id, recording_id, product_warehouse_id, project_flock_kandang_id) VALUES
			(101, 1, 501, NULL),
			(102, 2, 502, 201),
			(103, 3, 503, 101),
			(104, 4, 504, NULL),
			(105, 5, 505, 201),
			(106, 6, 506, NULL),
			(107, 7, 507, 201),
			(108, 8, 508, NULL),
			(109, 9, 509, 201),
			(110, 10, 510, NULL),
			(111, 11, 511, 201)`,
		`INSERT INTO purchase_items (id, product_id, price) VALUES
			(601, 10, 100),
			(602, 10, 110),
			(603, 10, 120),
			(604, 10, 130),
			(605, 10, 140),
			(606, 10, 150),
			(607, 10, 160),
			(608, 10, 170),
			(609, 10, 180),
			(610, 10, 190),
			(611, 10, 200)`,
		`INSERT INTO stock_allocations (id, usable_type, usable_id, stockable_type, stockable_id, status, allocation_purpose, qty) VALUES
			(9001, 'RECORDING_STOCK', 101, 'PURCHASE_ITEMS', 601, 'ACTIVE', 'CONSUME', 2),
			(9002, 'RECORDING_STOCK', 102, 'PURCHASE_ITEMS', 602, 'ACTIVE', 'CONSUME', 1),
			(9003, 'RECORDING_STOCK', 103, 'PURCHASE_ITEMS', 603, 'ACTIVE', 'CONSUME', 1),
			(9004, 'RECORDING_STOCK', 104, 'PURCHASE_ITEMS', 604, 'ACTIVE', 'CONSUME', 1),
			(9005, 'RECORDING_STOCK', 105, 'PURCHASE_ITEMS', 605, 'ACTIVE', 'CONSUME', 1),
			(9006, 'RECORDING_STOCK', 106, 'PURCHASE_ITEMS', 606, 'ACTIVE', 'CONSUME', 1),
			(9007, 'RECORDING_STOCK', 107, 'PURCHASE_ITEMS', 607, 'ACTIVE', 'CONSUME', 1),
			(9008, 'RECORDING_STOCK', 108, 'PURCHASE_ITEMS', 608, 'ACTIVE', 'CONSUME', 1),
			(9009, 'RECORDING_STOCK', 109, 'PURCHASE_ITEMS', 609, 'ACTIVE', 'CONSUME', 1),
			(9010, 'RECORDING_STOCK', 110, 'PURCHASE_ITEMS', 610, 'ACTIVE', 'CONSUME', 1),
			(9011, 'RECORDING_STOCK', 111, 'PURCHASE_ITEMS', 611, 'ACTIVE', 'CONSUME', 1)`,
		`INSERT INTO laying_transfers (id, transfer_date, effective_move_date, economic_cutoff_date, executed_at, deleted_at) VALUES
			(1001, '2026-04-04', '2026-04-05', NULL, '2026-04-05 00:00:00', NULL),
			(1002, '2026-05-01', '2026-05-01', NULL, '2026-05-01 00:00:00', NULL),
			(1003, '2026-04-03', '2026-04-05', NULL, '2026-04-05 00:00:00', NULL),
			(1004, '2026-04-03', '2026-04-05', NULL, NULL, NULL)`,
		`INSERT INTO laying_transfer_targets (id, laying_transfer_id, target_project_flock_kandang_id, deleted_at) VALUES
			(2001, 1001, 101, NULL),
			(2002, 1002, 103, NULL),
			(2003, 1003, 104, NULL),
			(2004, 1004, 105, NULL)`,
		fmt.Sprintf(`INSERT INTO approvals (id, approvable_type, approvable_id, action) VALUES
			(3001, '%s', 1001, 'APPROVED'),
			(3002, '%s', 1002, 'APPROVED'),
			(3003, '%s', 1003, 'APPROVED'),
			(3004, '%s', 1003, 'REJECTED'),
			(3005, '%s', 1004, 'APPROVED')`,
			approvalType, approvalType, approvalType, approvalType, approvalType),
	)

	repo := &HppV2RepositoryImpl{db: db}

	periodDate := mustJakartaTime(t, "2026-04-30 00:00:00")
	total, err := repo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(context.Background(), 1, periodDate)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	assertFloatEquals(t, total, 750)

	earlyPeriod := mustJakartaTime(t, "2026-04-10 23:59:59")
	earlyTotal, err := repo.GetRecordingStockRoutingAdjustmentCostByProjectFlockID(context.Background(), 1, earlyPeriod)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	assertFloatEquals(t, earlyTotal, 240)
}

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

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

	mustExecHppV2(t, db,
		`CREATE TABLE recordings (
			id INTEGER PRIMARY KEY,
			project_flock_kandangs_id INTEGER NULL,
			record_datetime DATETIME NULL,
			deleted_at DATETIME NULL
		)`,
		`CREATE TABLE recording_stocks (
			id INTEGER PRIMARY KEY,
			recording_id INTEGER NULL,
			product_warehouse_id INTEGER NULL,
			project_flock_kandang_id INTEGER NULL
		)`,
		`CREATE TABLE recording_eggs (
			id INTEGER PRIMARY KEY,
			recording_id INTEGER NULL,
			product_warehouse_id INTEGER NULL,
			qty NUMERIC(15,3) NULL,
			weight NUMERIC(15,3) NULL,
			project_flock_kandang_id INTEGER NULL
		)`,
		`CREATE TABLE stock_transfers (
			id INTEGER PRIMARY KEY,
			transfer_date DATETIME NULL
		)`,
		`CREATE TABLE stock_transfer_details (
			id INTEGER PRIMARY KEY,
			stock_transfer_id INTEGER NULL,
			source_product_warehouse_id INTEGER NULL,
			dest_product_warehouse_id INTEGER NULL
		)`,
		`CREATE TABLE stock_allocations (
			id INTEGER PRIMARY KEY,
			usable_type TEXT NULL,
			usable_id INTEGER NULL,
			stockable_type TEXT NULL,
			stockable_id INTEGER NULL,
			status TEXT NULL,
			allocation_purpose TEXT NULL,
			qty NUMERIC(15,3) NULL
		)`,
		`CREATE TABLE adjustment_stocks (
			id INTEGER PRIMARY KEY,
			product_warehouse_id INTEGER NULL,
			total_qty NUMERIC(15,3) NULL,
			usage_qty NUMERIC(15,3) NULL,
			price NUMERIC(15,3) NULL,
			grand_total NUMERIC(15,3) NULL,
			function_code TEXT NULL,
			transaction_type TEXT NULL,
			created_at DATETIME NULL
		)`,
		`CREATE TABLE kandangs (
			id INTEGER PRIMARY KEY,
			location_id INTEGER NULL
		)`,
		`CREATE TABLE project_flock_kandangs (
			id INTEGER PRIMARY KEY,
			kandang_id INTEGER NULL,
			project_flock_id INTEGER NULL
		)`,
		`CREATE TABLE warehouses (
			id INTEGER PRIMARY KEY,
			type TEXT NULL,
			location_id INTEGER NULL
		)`,
		`CREATE TABLE product_warehouses (
			id INTEGER PRIMARY KEY,
			warehouse_id INTEGER NULL,
			product_id INTEGER NULL,
			project_flock_kandang_id INTEGER NULL
		)`,
		`CREATE TABLE marketing_products (
			id INTEGER PRIMARY KEY,
			product_warehouse_id INTEGER NULL
		)`,
		`CREATE TABLE purchase_items (
			id INTEGER PRIMARY KEY,
			product_id INTEGER NULL,
			price NUMERIC(15,3) NULL
		)`,
		`CREATE TABLE marketing_delivery_products (
			id INTEGER PRIMARY KEY,
			marketing_product_id INTEGER NULL,
			usage_qty NUMERIC(15,3) NULL,
			total_weight NUMERIC(15,3) NULL,
			delivery_date DATETIME NULL
		)`,
		`CREATE TABLE flags (
			id INTEGER PRIMARY KEY,
			flagable_type TEXT NULL,
			flagable_id INTEGER NULL,
			name TEXT NULL
		)`,
		`CREATE TABLE laying_transfers (
			id INTEGER PRIMARY KEY,
			transfer_date DATETIME NULL,
			effective_move_date DATETIME NULL,
			economic_cutoff_date DATETIME NULL,
			executed_at DATETIME NULL,
			deleted_at DATETIME NULL
		)`,
		`CREATE TABLE laying_transfer_targets (
			id INTEGER PRIMARY KEY,
			laying_transfer_id INTEGER NULL,
			target_project_flock_kandang_id INTEGER NULL,
			deleted_at DATETIME NULL
		)`,
		`CREATE TABLE approvals (
			id INTEGER PRIMARY KEY,
			approvable_type TEXT NULL,
			approvable_id INTEGER NULL,
			action TEXT NULL
		)`,
	)

	return db
}

func mustExecHppV2(t *testing.T, db *gorm.DB, statements ...string) {
	t.Helper()

	for _, statement := range statements {
		if err := db.Exec(statement).Error; err != nil {
			t.Fatalf("failed executing statement %q: %v", statement, err)
		}
	}
}

func mustJakartaTime(t *testing.T, raw string) time.Time {
	t.Helper()

	location, err := time.LoadLocation("Asia/Jakarta")
	if err != nil {
		t.Fatalf("failed loading timezone: %v", err)
	}

	value, err := time.ParseInLocation("2006-01-02 15:04:05", raw, location)
	if err != nil {
		t.Fatalf("failed parsing time %q: %v", raw, err)
	}

	return value
}

func assertFloatEquals(t *testing.T, got float64, want float64) {
	t.Helper()

	if math.Abs(got-want) > 0.000001 {
		t.Fatalf("expected %.6f, got %.6f", want, got)
	}
}

func TestHppV2RepositoryConstantsStayAlignedWithProductionQueries(t *testing.T) {
	if fifo.UsableKeyStockTransferOut.String() != "STOCK_TRANSFER_OUT" {
		t.Fatalf("unexpected stock transfer usable key: %s", fifo.UsableKeyStockTransferOut.String())
	}
	if fifo.StockableKeyAdjustmentIn.String() != "ADJUSTMENT_IN" {
		t.Fatalf("unexpected adjustment stockable key: %s", fifo.StockableKeyAdjustmentIn.String())
	}
	if entity.StockAllocationStatusActive != "ACTIVE" {
		t.Fatalf("unexpected active stock allocation status: %s", entity.StockAllocationStatusActive)
	}
	if entity.StockAllocationPurposeConsume != "CONSUME" {
		t.Fatalf("unexpected consume stock allocation purpose: %s", entity.StockAllocationPurposeConsume)
	}
	if string(utils.AdjustmentTransactionSubtypeRecordingEggIn) != "RECORDING_EGG_IN" {
		t.Fatalf("unexpected adjustment function code: %s", utils.AdjustmentTransactionSubtypeRecordingEggIn)
	}
	if string(utils.AdjustmentTransactionTypeRecording) != "RECORDING" {
		t.Fatalf("unexpected adjustment transaction type: %s", utils.AdjustmentTransactionTypeRecording)
	}
}
