package finance

import (
	"context"
	"errors"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/shopspring/decimal"

	"github.com/TaruLab/keluargamart-backend/internal/db"
)

func mustDate(t *testing.T, value string) time.Time {
	t.Helper()
	ts, err := time.Parse("2006-01-02", value)
	if err != nil {
		t.Fatal(err)
	}
	return ts
}

func TestCashflowDatabaseGuards(t *testing.T) {
	databaseURL := os.Getenv("DATABASE_URL")
	if databaseURL == "" {
		t.Skip("DATABASE_URL is not set")
	}

	ctx := context.Background()
	pool, err := pgxpool.New(ctx, databaseURL)
	if err != nil {
		t.Fatal(err)
	}
	defer pool.Close()

	tx, err := pool.Begin(ctx)
	if err != nil {
		t.Fatal(err)
	}
	defer tx.Rollback(ctx)

	if _, err := tx.Exec(ctx, `INSERT INTO accounting_periods (period_year, period_month, status)
		VALUES (2098, 1, 'locked') ON CONFLICT (period_year, period_month) DO UPDATE SET status='locked'`); err != nil {
		t.Fatal(err)
	}

	repo := NewRepository(pool)
	locked := db.CreateCashflowParams{
		TransactionNumber: "CF-INTEGRATION-LOCKED", Type: "income", Category: "other",
		TransactionDate: pgtype.Date{Time: mustDate(t, "2098-01-15"), Valid: true}, Amount: decimal.NewFromInt(1),
	}
	if _, err := tx.Exec(ctx, "SAVEPOINT before_locked"); err != nil {
		t.Fatal(err)
	}
	if _, err := repo.CreateCashflow(ctx, tx, locked); !errors.Is(err, ErrAccountingPeriodLocked) {
		t.Fatalf("locked-period create error = %v, want ErrAccountingPeriodLocked", err)
	}
	if _, err := tx.Exec(ctx, "ROLLBACK TO SAVEPOINT before_locked"); err != nil {
		t.Fatal(err)
	}

	generated := db.CreateCashflowParams{
		TransactionNumber: "CF-INTEGRATION-GENERATED", Type: "income", Category: "sales",
		TransactionDate: pgtype.Date{Time: mustDate(t, "2098-02-15"), Valid: true}, Amount: decimal.NewFromInt(10),
		ReferenceType: pgtype.Text{String: InvoiceModel, Valid: true}, ReferenceID: pgtype.Int8{Int64: 999999, Valid: true},
	}
	id, err := repo.CreateCashflow(ctx, tx, generated)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := tx.Exec(ctx, "SAVEPOINT before_update"); err != nil {
		t.Fatal(err)
	}
	_, err = repo.UpdateCashflow(ctx, tx, db.UpdateCashflowParams{
		ID: id, Type: generated.Type, Category: generated.Category, TransactionDate: generated.TransactionDate,
		Amount: decimal.NewFromInt(11),
	})
	if !errors.Is(err, ErrGeneratedCashflowImmutable) {
		t.Fatalf("generated amount update error = %v, want ErrGeneratedCashflowImmutable", err)
	}
	if _, err := tx.Exec(ctx, "ROLLBACK TO SAVEPOINT before_update"); err != nil {
		t.Fatal(err)
	}
	if _, err := tx.Exec(ctx, "SAVEPOINT before_delete"); err != nil {
		t.Fatal(err)
	}
	if err := repo.DeleteCashflow(ctx, tx, id); !errors.Is(err, ErrGeneratedCashflowImmutable) {
		t.Fatalf("generated delete error = %v, want ErrGeneratedCashflowImmutable", err)
	}
	if _, err := tx.Exec(ctx, "ROLLBACK TO SAVEPOINT before_delete"); err != nil {
		t.Fatal(err)
	}

	if err := tx.Rollback(ctx); err != nil {
		t.Fatal(err)
	}
}

func TestJournalDatabaseGuardRejectsLockedPeriod(t *testing.T) {
	databaseURL := os.Getenv("DATABASE_URL")
	if databaseURL == "" {
		t.Skip("DATABASE_URL is not set")
	}
	ctx := context.Background()
	pool, err := pgxpool.New(ctx, databaseURL)
	if err != nil {
		t.Fatal(err)
	}
	defer pool.Close()

	tx, err := pool.Begin(ctx)
	if err != nil {
		t.Fatal(err)
	}
	defer tx.Rollback(ctx)

	if _, err := tx.Exec(ctx, `INSERT INTO accounting_periods (period_year, period_month, status)
		VALUES (2098, 3, 'locked') ON CONFLICT (period_year, period_month) DO UPDATE SET status='locked'`); err != nil {
		t.Fatal(err)
	}
	if _, err := tx.Exec(ctx, "SAVEPOINT before_journal"); err != nil {
		t.Fatal(err)
	}
	_, err = tx.Exec(ctx, `INSERT INTO journal_entries
		(journal_number, journal_type, entry_date, status, total_debit, total_credit)
		VALUES ('JU-INTEGRATION-LOCKED', 'general', '2098-03-15', 'draft', 1, 1)`)
	if err == nil || !strings.Contains(err.Error(), "accounting_period_locked") {
		t.Fatalf("locked-period journal insert error = %v, want accounting_period_locked", err)
	}
	if _, err := tx.Exec(ctx, "ROLLBACK TO SAVEPOINT before_journal"); err != nil {
		t.Fatal(err)
	}
}

func TestJournalSourceIdempotencyMigrationIsScoped(t *testing.T) {
	data, err := os.ReadFile("../../migrations/000127_journal_source_idempotency.sql")
	if err != nil {
		t.Fatal(err)
	}
	s := string(data)
	for _, want := range []string{"goods_receipt", "payroll", "CREATE UNIQUE INDEX", "HAVING count(*) > 1"} {
		if !strings.Contains(s, want) {
			t.Fatalf("idempotency migration missing %q", want)
		}
	}
	if strings.Contains(s, "DELETE FROM journal_entries") {
		t.Fatal("idempotency migration must not delete financial history")
	}
}

func TestCloseAccountingPeriodUsesJournalTablesAndBlocksDrafts(t *testing.T) {
	databaseURL := os.Getenv("DATABASE_URL")
	if databaseURL == "" {
		t.Skip("DATABASE_URL is not set")
	}
	ctx := context.Background()
	pool, err := pgxpool.New(ctx, databaseURL)
	if err != nil {
		t.Fatal(err)
	}

	const year, month = int16(2198), int16(4)
	const journalNumber = "CLOSE-DRAFT-219804"
	var actorID int64
	testEmail := "accounting-period-test-" + time.Now().UTC().Format("20060102150405.000000000") + "@example.invalid"
	if err := pool.QueryRow(ctx, `INSERT INTO users(name,email,password,role) VALUES ('Accounting Test',$1,'unused','admin') RETURNING id`, testEmail).Scan(&actorID); err != nil {
		t.Fatal(err)
	}
	if _, err := pool.Exec(ctx, `INSERT INTO accounting_periods(period_year,period_month,status) VALUES ($1,$2,'open')`, year, month); err != nil {
		_, _ = pool.Exec(ctx, `DELETE FROM users WHERE id=$1`, actorID)
		t.Fatal(err)
	}
	if _, err := pool.Exec(ctx, `INSERT INTO journal_entries(journal_number,journal_type,entry_date,status,total_debit,total_credit) VALUES ($1,'general','2198-04-15','draft',1,1)`, journalNumber); err != nil {
		_, _ = pool.Exec(ctx, `DELETE FROM accounting_periods WHERE period_year=$1 AND period_month=$2`, year, month)
		_, _ = pool.Exec(ctx, `DELETE FROM users WHERE id=$1`, actorID)
		t.Fatal(err)
	}
	t.Cleanup(pool.Close)
	t.Cleanup(func() {
		_, _ = pool.Exec(context.Background(), `DELETE FROM journal_entries WHERE journal_number=$1`, journalNumber)
		_, _ = pool.Exec(context.Background(), `DELETE FROM accounting_periods WHERE period_year=$1 AND period_month=$2`, year, month)
		_, _ = pool.Exec(context.Background(), `DELETE FROM users WHERE id=$1`, actorID)
	})

	repo := NewRepository(pool)
	_, err = repo.CloseAccountingPeriod(ctx, year, month, actorID)
	if !errors.Is(err, ErrPeriodCloseBlocked) {
		t.Fatalf("close with draft journal error = %v, want ErrPeriodCloseBlocked", err)
	}
	var status string
	if err := pool.QueryRow(ctx, `SELECT status FROM accounting_periods WHERE period_year=$1 AND period_month=$2`, year, month).Scan(&status); err != nil {
		t.Fatal(err)
	}
	if status != "open" {
		t.Fatalf("period status after failed close = %q, want open", status)
	}
}
