package services

// Pure-Go validator tests for LayoutSpec. No DB required.

import (
	"encoding/json"
	"errors"
	"testing"
)

func TestDefaultLayoutSpec_IsValid(t *testing.T) {
	if err := DefaultLayoutSpec().Validate(); err != nil {
		t.Fatalf("DefaultLayoutSpec invalid: %v", err)
	}
}

func TestLayoutSpec_RejectsEmpty(t *testing.T) {
	s := LayoutSpec{
		Density:     CardDensityRoomy,
		GridColumns: GridAuto,
	}
	err := s.Validate()
	if !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("empty facts: got %v, want ErrInvalidInput", err)
	}
}

func TestLayoutSpec_RequiresTitleRowFirst(t *testing.T) {
	s := LayoutSpec{
		Facts: []LayoutFact{
			{Key: FactTypeChip, Visible: true},
			{Key: FactTitleRow, Visible: true},
		},
		Density:     CardDensityRoomy,
		GridColumns: GridAuto,
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("title-row not first: got %v, want ErrInvalidInput", err)
	}
}

func TestLayoutSpec_AllowsTitleRowAfterHiddenLeading(t *testing.T) {
	// type-chip is hidden so it doesn't count; title-row is the first VISIBLE.
	s := LayoutSpec{
		Facts: []LayoutFact{
			{Key: FactTypeChip, Visible: false},
			{Key: FactTitleRow, Visible: true},
			{Key: FactStatusChip, Visible: true},
		},
		Density:     CardDensityRoomy,
		GridColumns: GridAuto,
	}
	if err := s.Validate(); err != nil {
		t.Fatalf("title-row first-visible should be ok; got %v", err)
	}
}

func TestLayoutSpec_RejectsDuplicateKey(t *testing.T) {
	s := LayoutSpec{
		Facts: []LayoutFact{
			{Key: FactTitleRow, Visible: true},
			{Key: FactTypeChip, Visible: true},
			{Key: FactTypeChip, Visible: true},
		},
		Density:     CardDensityRoomy,
		GridColumns: GridAuto,
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("duplicate key: got %v, want ErrInvalidInput", err)
	}
}

func TestLayoutSpec_RejectsUnknownKey(t *testing.T) {
	s := LayoutSpec{
		Facts: []LayoutFact{
			{Key: FactTitleRow, Visible: true},
			{Key: "made-up-fact", Visible: true},
		},
		Density:     CardDensityRoomy,
		GridColumns: GridAuto,
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("unknown key: got %v, want ErrInvalidInput", err)
	}
}

func TestLayoutSpec_CountBoundsAndPlacement(t *testing.T) {
	bad := 7
	s := LayoutSpec{
		Facts: []LayoutFact{
			{Key: FactTitleRow, Visible: true},
			{Key: FactNextEvents, Visible: true, Count: &bad},
		},
		Density:     CardDensityRoomy,
		GridColumns: GridAuto,
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("count out-of-range: got %v, want ErrInvalidInput", err)
	}

	// count on a key that doesn't accept it.
	good := 3
	s = LayoutSpec{
		Facts: []LayoutFact{
			{Key: FactTitleRow, Visible: true},
			{Key: FactStatusChip, Visible: true, Count: &good},
		},
		Density:     CardDensityRoomy,
		GridColumns: GridAuto,
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("count on wrong key: got %v, want ErrInvalidInput", err)
	}
}

func TestLayoutSpec_DensityAndGrid(t *testing.T) {
	s := LayoutSpec{
		Facts:       []LayoutFact{{Key: FactTitleRow, Visible: true}},
		Density:     "spacious", // invalid
		GridColumns: GridAuto,
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("invalid density: got %v, want ErrInvalidInput", err)
	}
	s.Density = CardDensityRoomy
	s.GridColumns = "5" // invalid
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("invalid grid: got %v, want ErrInvalidInput", err)
	}
}

func TestParseLayoutSpec_RoundTrip(t *testing.T) {
	in := DefaultLayoutSpec()
	bytes := mustJSON(t, in)
	out, err := ParseLayoutSpec(bytes)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if len(out.Facts) != len(in.Facts) {
		t.Errorf("facts len = %d, want %d", len(out.Facts), len(in.Facts))
	}
	if out.Density != in.Density || out.GridColumns != in.GridColumns {
		t.Errorf("density/grid drift: out=(%s, %s) in=(%s, %s)",
			out.Density, out.GridColumns, in.Density, in.GridColumns)
	}
}

func mustJSON(t *testing.T, v any) []byte {
	t.Helper()
	b, err := json.Marshal(v)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	return b
}
