package main

import (
	"context"
	"errors"
	"fmt"
	"path/filepath"
	"strings"
	"testing"

	"github.com/xuri/excelize/v2"
)

func TestParseAdjustmentPriceFile_ValidSingleRow(t *testing.T) {
	filePath := createWorkbook(
		t,
		"adjustment_prices",
		[]string{"adjustment_id", "weight"},
		[][]string{{"101", "12.345"}},
	)

	sheet, rows, issues, err := parseAdjustmentPriceFile(filePath, "")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if sheet != "adjustment_prices" {
		t.Fatalf("expected selected sheet adjustment_prices, got %q", sheet)
	}
	if len(issues) != 0 {
		t.Fatalf("expected no issues, got %+v", issues)
	}
	if len(rows) != 1 {
		t.Fatalf("expected 1 row, got %d", len(rows))
	}
	if rows[0].AdjustmentID != 101 {
		t.Fatalf("expected adjustment_id 101, got %d", rows[0].AdjustmentID)
	}
	if rows[0].Weight != 12.345 {
		t.Fatalf("expected weight 12.345, got %v", rows[0].Weight)
	}
}

func TestParseAdjustmentPriceFile_ValidMultiRow(t *testing.T) {
	filePath := createWorkbook(
		t,
		"adjustment_prices",
		[]string{" Adjustment_ID ", "WEIGHT"},
		[][]string{{"101", "10"}, {"102", "11.5"}},
	)

	_, rows, issues, err := parseAdjustmentPriceFile(filePath, "adjustment_prices")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(issues) != 0 {
		t.Fatalf("expected no issues, got %+v", issues)
	}
	if len(rows) != 2 {
		t.Fatalf("expected 2 rows, got %d", len(rows))
	}
}

func TestParseAdjustmentPriceFile_MissingRequiredHeader(t *testing.T) {
	filePath := createWorkbook(
		t,
		"adjustment_prices",
		[]string{"adjustment_id", "price"},
		[][]string{{"101", "12"}},
	)

	_, rows, issues, err := parseAdjustmentPriceFile(filePath, "")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(rows) != 0 {
		t.Fatalf("expected 0 parsed rows when header invalid, got %d", len(rows))
	}
	if !hasIssue(issues, 0, "weight", "required header is missing") {
		t.Fatalf("expected missing weight header issue, got %+v", issues)
	}
}

func TestParseAdjustmentPriceFile_InvalidAdjustmentID(t *testing.T) {
	filePath := createWorkbook(
		t,
		"adjustment_prices",
		[]string{"adjustment_id", "weight"},
		[][]string{{"abc", "10"}, {"0", "12"}},
	)

	_, rows, issues, err := parseAdjustmentPriceFile(filePath, "")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(rows) != 0 {
		t.Fatalf("expected no valid rows, got %d", len(rows))
	}
	if !hasIssue(issues, 2, "adjustment_id", "must be a positive integer") {
		t.Fatalf("expected non numeric adjustment_id issue, got %+v", issues)
	}
	if !hasIssue(issues, 3, "adjustment_id", "must be greater than 0") {
		t.Fatalf("expected adjustment_id >0 issue, got %+v", issues)
	}
}

func TestParseAdjustmentPriceFile_InvalidWeight(t *testing.T) {
	filePath := createWorkbook(
		t,
		"adjustment_prices",
		[]string{"adjustment_id", "weight"},
		[][]string{{"101", "abc"}, {"102", "-1"}},
	)

	_, rows, issues, err := parseAdjustmentPriceFile(filePath, "")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(rows) != 0 {
		t.Fatalf("expected no valid rows, got %d", len(rows))
	}
	if !hasIssue(issues, 2, "weight", "must be numeric") {
		t.Fatalf("expected weight numeric issue, got %+v", issues)
	}
	if !hasIssue(issues, 3, "weight", "must be greater than or equal to 0") {
		t.Fatalf("expected weight >=0 issue, got %+v", issues)
	}
}

func TestParseAdjustmentPriceFile_DuplicateAdjustmentID_LastRowWins(t *testing.T) {
	filePath := createWorkbook(
		t,
		"adjustment_prices",
		[]string{"adjustment_id", "weight"},
		[][]string{{"101", "10"}, {"102", "20"}, {"101", "30"}},
	)

	_, rows, issues, err := parseAdjustmentPriceFile(filePath, "")
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(issues) != 0 {
		t.Fatalf("expected no issues, got %+v", issues)
	}
	if len(rows) != 2 {
		t.Fatalf("expected 2 deduped rows, got %d", len(rows))
	}

	row101, ok := findRowByAdjustmentID(rows, 101)
	if !ok {
		t.Fatalf("expected adjustment_id 101 to exist, got %+v", rows)
	}
	if row101.Weight != 30 {
		t.Fatalf("expected duplicate adjustment_id to keep last weight 30, got %v", row101.Weight)
	}
	if row101.RowNumber != 4 {
		t.Fatalf("expected duplicate adjustment_id to keep last row number 4, got %d", row101.RowNumber)
	}
}

func TestSplitRowsByExistingIDs_SkipMissing(t *testing.T) {
	rows := []adjustmentPriceImportRow{
		{RowNumber: 2, AdjustmentID: 101, Weight: 10},
		{RowNumber: 3, AdjustmentID: 102, Weight: 11},
		{RowNumber: 4, AdjustmentID: 103, Weight: 12},
	}
	existing := map[uint]struct{}{101: {}, 103: {}}

	processable, skipped := splitRowsByExistingIDs(rows, existing)
	if len(processable) != 2 {
		t.Fatalf("expected 2 processable rows, got %d", len(processable))
	}
	if len(skipped) != 1 {
		t.Fatalf("expected 1 skipped row, got %d", len(skipped))
	}
	if skipped[0].AdjustmentID != 102 {
		t.Fatalf("expected adjustment_id 102 skipped, got %+v", skipped)
	}
}

func TestApplyIfRequested_DryRunDoesNotWrite(t *testing.T) {
	runner := &fakeTransactionRunner{}
	rows := []adjustmentPriceImportRow{{RowNumber: 2, AdjustmentID: 101, Weight: 10}}

	results, err := applyIfRequested(context.Background(), false, runner, rows)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if results != nil {
		t.Fatalf("expected nil results on dry-run, got %+v", results)
	}
	if runner.txCalls != 0 {
		t.Fatalf("expected no transaction call during dry-run, got %d", runner.txCalls)
	}
}

func TestApplyImportRows_Success(t *testing.T) {
	runner := &fakeTransactionRunner{
		changedByID: map[uint]bool{101: true, 102: false},
	}
	rows := []adjustmentPriceImportRow{
		{RowNumber: 2, AdjustmentID: 101, Weight: 10},
		{RowNumber: 3, AdjustmentID: 102, Weight: 11},
	}

	results, err := applyImportRows(context.Background(), runner, rows)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if runner.txCalls != 1 {
		t.Fatalf("expected 1 transaction call, got %d", runner.txCalls)
	}
	if len(runner.committedCalls) != 2 {
		t.Fatalf("expected 2 committed updates, got %d", len(runner.committedCalls))
	}
	if len(results) != 2 {
		t.Fatalf("expected 2 row results, got %d", len(results))
	}
	if !results[0].Changed || results[1].Changed {
		t.Fatalf("unexpected changed flags: %+v", results)
	}
}

func TestApplyImportRows_RollbackOnError(t *testing.T) {
	runner := &fakeTransactionRunner{
		errByID: map[uint]error{102: errors.New("boom")},
	}
	rows := []adjustmentPriceImportRow{
		{RowNumber: 2, AdjustmentID: 101, Weight: 10},
		{RowNumber: 3, AdjustmentID: 102, Weight: 11},
	}

	_, err := applyImportRows(context.Background(), runner, rows)
	if err == nil {
		t.Fatal("expected error due to update failure")
	}
	if !strings.Contains(err.Error(), "row 3 adjustment_id=102 update failed") {
		t.Fatalf("unexpected error message: %v", err)
	}
	if runner.txCalls != 1 {
		t.Fatalf("expected 1 transaction call, got %d", runner.txCalls)
	}
	if len(runner.committedCalls) != 0 {
		t.Fatalf("expected no committed updates on rollback, got %d", len(runner.committedCalls))
	}
}

func createWorkbook(t *testing.T, sheetName string, headers []string, rows [][]string) string {
	t.Helper()

	f := excelize.NewFile()
	defaultSheet := f.GetSheetName(f.GetActiveSheetIndex())
	if sheetName == "" {
		sheetName = defaultSheet
	} else if sheetName != defaultSheet {
		f.SetSheetName(defaultSheet, sheetName)
	}

	for idx, header := range headers {
		cell, err := excelize.CoordinatesToCellName(idx+1, 1)
		if err != nil {
			t.Fatalf("failed resolving header cell: %v", err)
		}
		if err := f.SetCellValue(sheetName, cell, header); err != nil {
			t.Fatalf("failed setting header cell: %v", err)
		}
	}

	for rowIdx, row := range rows {
		for colIdx, value := range row {
			cell, err := excelize.CoordinatesToCellName(colIdx+1, rowIdx+2)
			if err != nil {
				t.Fatalf("failed resolving data cell: %v", err)
			}
			if err := f.SetCellValue(sheetName, cell, value); err != nil {
				t.Fatalf("failed setting data cell: %v", err)
			}
		}
	}

	path := filepath.Join(t.TempDir(), "adjustment_prices.xlsx")
	if err := f.SaveAs(path); err != nil {
		t.Fatalf("failed saving workbook: %v", err)
	}
	if err := f.Close(); err != nil {
		t.Fatalf("failed closing workbook: %v", err)
	}

	return path
}

func hasIssue(issues []validationIssue, row int, field, messageContains string) bool {
	for _, issue := range issues {
		if issue.Row != row {
			continue
		}
		if issue.Field != field {
			continue
		}
		if strings.Contains(issue.Message, messageContains) {
			return true
		}
	}
	return false
}

func findRowByAdjustmentID(rows []adjustmentPriceImportRow, adjustmentID uint) (adjustmentPriceImportRow, bool) {
	for _, row := range rows {
		if row.AdjustmentID == adjustmentID {
			return row, true
		}
	}
	return adjustmentPriceImportRow{}, false
}

type updateCall struct {
	adjustmentID uint
	price        float64
}

type fakeAdjustmentPriceStore struct {
	changedByID map[uint]bool
	errByID     map[uint]error
	calls       []updateCall
}

func (s *fakeAdjustmentPriceStore) UpdatePrice(_ context.Context, adjustmentID uint, price float64) (bool, error) {
	s.calls = append(s.calls, updateCall{adjustmentID: adjustmentID, price: price})
	if err, exists := s.errByID[adjustmentID]; exists {
		return false, fmt.Errorf("forced update failure for adjustment_id=%d: %w", adjustmentID, err)
	}
	if changed, exists := s.changedByID[adjustmentID]; exists {
		return changed, nil
	}
	return true, nil
}

type fakeTransactionRunner struct {
	txCalls        int
	changedByID    map[uint]bool
	errByID        map[uint]error
	committedCalls []updateCall
}

func (r *fakeTransactionRunner) InTx(ctx context.Context, fn func(store adjustmentPriceStore) error) error {
	r.txCalls++

	txStore := &fakeAdjustmentPriceStore{
		changedByID: r.changedByID,
		errByID:     r.errByID,
		calls:       make([]updateCall, 0),
	}

	if err := fn(txStore); err != nil {
		return err
	}

	r.committedCalls = append(r.committedCalls, txStore.calls...)
	return nil
}

var _ txRunner = (*fakeTransactionRunner)(nil)
var _ adjustmentPriceStore = (*fakeAdjustmentPriceStore)(nil)
