package orm

import (
	"fmt"
	"testing"
)

func TestNewModel(t *testing.T) {
	m := NewModel("test.model.unit.new", ModelOpts{
		Description: "Test Model",
		RecName:     "name",
	})
	if m.Name() != "test.model.unit.new" {
		t.Errorf("name: %s", m.Name())
	}
	if m.Table() != "test_model_unit_new" {
		t.Errorf("table: %s", m.Table())
	}
	if m.Description() != "Test Model" {
		t.Errorf("desc: %s", m.Description())
	}
	if m.RecName() != "name" {
		t.Errorf("rec_name: %s", m.RecName())
	}
}

func TestNewModelDefaults(t *testing.T) {
	m := NewModel("test.model.defaults", ModelOpts{})
	if m.Order() != "id" {
		t.Errorf("default order: %s", m.Order())
	}
	if m.RecName() != "name" {
		t.Errorf("default rec_name: %s", m.RecName())
	}
	if m.IsAbstract() {
		t.Error("should not be abstract")
	}
	if m.IsTransient() {
		t.Error("should not be transient")
	}
}

func TestNewModelCustomTable(t *testing.T) {
	m := NewModel("test.model.custom.table", ModelOpts{
		Table: "my_custom_table",
	})
	if m.Table() != "my_custom_table" {
		t.Errorf("table: %s", m.Table())
	}
}

func TestNewModelAbstract(t *testing.T) {
	m := NewModel("test.model.abstract", ModelOpts{
		Type: ModelAbstract,
	})
	if m.IsAbstract() != true {
		t.Error("should be abstract")
	}
	// Abstract models have no table
	if m.Table() != "" {
		t.Errorf("abstract should have no table, got %s", m.Table())
	}
}

func TestNewModelTransient(t *testing.T) {
	m := NewModel("test.model.transient", ModelOpts{
		Type: ModelTransient,
	})
	if m.IsTransient() != true {
		t.Error("should be transient")
	}
}

func TestModelMagicFields(t *testing.T) {
	m := NewModel("test.model.magic", ModelOpts{})
	// Magic fields should be auto-created
	if f := m.GetField("id"); f == nil {
		t.Error("id field missing")
	}
	if f := m.GetField("display_name"); f == nil {
		t.Error("display_name field missing")
	}
	if f := m.GetField("create_uid"); f == nil {
		t.Error("create_uid field missing")
	}
	if f := m.GetField("create_date"); f == nil {
		t.Error("create_date field missing")
	}
	if f := m.GetField("write_uid"); f == nil {
		t.Error("write_uid field missing")
	}
	if f := m.GetField("write_date"); f == nil {
		t.Error("write_date field missing")
	}
}

func TestModelAddFields(t *testing.T) {
	m := NewModel("test.model.fields.add", ModelOpts{})
	m.AddFields(
		Char("name", FieldOpts{String: "Name", Required: true}),
		Integer("age", FieldOpts{String: "Age"}),
		Boolean("active", FieldOpts{String: "Active", Default: true}),
	)

	if f := m.GetField("name"); f == nil {
		t.Error("name field missing")
	}
	if f := m.GetField("age"); f == nil {
		t.Error("age field missing")
	}
	if f := m.GetField("active"); f == nil {
		t.Error("active field missing")
	}
	if f := m.GetField("nonexistent"); f != nil {
		t.Error("should be nil")
	}

	nameF := m.GetField("name")
	if nameF.Type != TypeChar {
		t.Error("expected char")
	}
	if !nameF.Required {
		t.Error("expected required")
	}
	if nameF.String != "Name" {
		t.Error("expected Name label")
	}
}

func TestModelAddFieldSetsModel(t *testing.T) {
	m := NewModel("test.model.field.backref", ModelOpts{})
	f := Char("ref", FieldOpts{})
	m.AddField(f)
	if f.model != m {
		t.Error("field should have back-reference to model")
	}
}

func TestModelStoredFields(t *testing.T) {
	m := NewModel("test.model.stored", ModelOpts{})
	m.AddFields(
		Char("name", FieldOpts{}),
		Char("computed_field", FieldOpts{Compute: "x"}),
		One2many("lines", "other.model", "parent_id", FieldOpts{}),
	)

	stored := m.StoredFields()
	// Should include magic fields + name, but not computed_field or o2m
	nameFound := false
	computedFound := false
	linesFound := false
	for _, f := range stored {
		switch f.Name {
		case "name":
			nameFound = true
		case "computed_field":
			computedFound = true
		case "lines":
			linesFound = true
		}
	}
	if !nameFound {
		t.Error("name should be in stored fields")
	}
	if computedFound {
		t.Error("computed_field should not be in stored fields")
	}
	if linesFound {
		t.Error("o2m lines should not be in stored fields")
	}
}

func TestModelRegisterMethod(t *testing.T) {
	m := NewModel("test.model.methods.reg", ModelOpts{})
	called := false
	m.RegisterMethod("test_action", func(rs *Recordset, args ...interface{}) (interface{}, error) {
		called = true
		return "ok", nil
	})

	if _, ok := m.Methods["test_action"]; !ok {
		t.Error("method not registered")
	}

	result, err := m.Methods["test_action"](nil)
	if err != nil {
		t.Fatal(err)
	}
	if result != "ok" {
		t.Error("expected ok")
	}
	if !called {
		t.Error("method not called")
	}
}

func TestExtendModel(t *testing.T) {
	NewModel("test.model.base.ext", ModelOpts{Description: "Base"})

	ext := ExtendModel("test.model.base.ext")
	ext.AddFields(Char("extra_field", FieldOpts{String: "Extra"}))

	base := Registry.Get("test.model.base.ext")
	if f := base.GetField("extra_field"); f == nil {
		t.Error("extension field missing")
	}
}

func TestExtendModelPanics(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Error("expected panic for missing model")
		}
	}()
	ExtendModel("nonexistent.model.xyz.panic")
}

func TestRegistryGet(t *testing.T) {
	NewModel("test.registry.get.model", ModelOpts{})
	if m := Registry.Get("test.registry.get.model"); m == nil {
		t.Error("model not found")
	}
	if m := Registry.Get("nonexistent.registry.model"); m != nil {
		t.Error("should be nil")
	}
}

func TestRegistryMustGetPanics(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Error("expected panic for missing model")
		}
	}()
	Registry.MustGet("nonexistent.mustget.model")
}

func TestRegistryAll(t *testing.T) {
	NewModel("test.registry.all.model", ModelOpts{})
	all := Registry.All()
	found := false
	for _, name := range all {
		if name == "test.registry.all.model" {
			found = true
			break
		}
	}
	if !found {
		t.Error("model not in Registry.All()")
	}
}

func TestRegistryModels(t *testing.T) {
	NewModel("test.registry.models.model", ModelOpts{})
	models := Registry.Models()
	if _, ok := models["test.registry.models.model"]; !ok {
		t.Error("model not in Registry.Models()")
	}
}

func TestModelSQLConstraint(t *testing.T) {
	m := NewModel("test.model.constraint.sql", ModelOpts{})
	m.AddSQLConstraint("unique_name", "UNIQUE(name)", "Name must be unique")
	if len(m.SQLConstraints) != 1 {
		t.Error("constraint not added")
	}
	if m.SQLConstraints[0].Name != "unique_name" {
		t.Error("wrong name")
	}
	if m.SQLConstraints[0].Definition != "UNIQUE(name)" {
		t.Error("wrong definition")
	}
	if m.SQLConstraints[0].Message != "Name must be unique" {
		t.Error("wrong message")
	}
}

func TestModelAddConstraint(t *testing.T) {
	m := NewModel("test.model.constraint.func", ModelOpts{})
	m.AddConstraint(func(rs *Recordset) error {
		return fmt.Errorf("test error")
	})
	if len(m.Constraints) != 1 {
		t.Error("constraint not added")
	}
}

func TestModelRegisterOnchange(t *testing.T) {
	m := NewModel("test.model.onchange", ModelOpts{})
	m.RegisterOnchange("partner_id", func(env *Environment, vals Values) Values {
		return Values{"name": "changed"}
	})
	if m.OnchangeHandlers == nil {
		t.Fatal("OnchangeHandlers should not be nil")
	}
	if _, ok := m.OnchangeHandlers["partner_id"]; !ok {
		t.Error("onchange handler not registered")
	}
}

func TestModelRegisterInverse(t *testing.T) {
	m := NewModel("test.model.inverse", ModelOpts{})
	m.AddFields(Char("computed", FieldOpts{Compute: "_compute_computed"}))
	m.RegisterInverse("computed", func(rs *Recordset, args ...interface{}) (interface{}, error) {
		return nil, nil
	})

	f := m.GetField("computed")
	if f.Inverse != "_inverse_computed" {
		t.Errorf("expected _inverse_computed, got %s", f.Inverse)
	}
	if _, ok := m.Methods["_inverse_computed"]; !ok {
		t.Error("inverse method not registered")
	}
}

func TestModelCreateTableSQL(t *testing.T) {
	m := NewModel("test.model.ddl", ModelOpts{})
	m.AddFields(
		Char("name", FieldOpts{Required: true}),
		Integer("count", FieldOpts{}),
	)

	sql := m.CreateTableSQL()
	if sql == "" {
		t.Fatal("expected non-empty SQL")
	}
	// Should contain the table name
	if !containsStr(sql, `"test_model_ddl"`) {
		t.Error("missing table name in DDL")
	}
	// Should contain name column
	if !containsStr(sql, `"name"`) {
		t.Error("missing name column in DDL")
	}
	// Should contain NOT NULL for required
	if !containsStr(sql, "NOT NULL") {
		t.Error("missing NOT NULL for required field")
	}
}

func TestModelCreateTableSQLAbstract(t *testing.T) {
	m := NewModel("test.model.ddl.abstract", ModelOpts{Type: ModelAbstract})
	sql := m.CreateTableSQL()
	if sql != "" {
		t.Error("abstract model should have empty DDL")
	}
}

func TestModelFields(t *testing.T) {
	m := NewModel("test.model.all.fields", ModelOpts{})
	m.AddFields(Char("name", FieldOpts{}))
	fields := m.Fields()
	if _, ok := fields["name"]; !ok {
		t.Error("name not in Fields()")
	}
	if _, ok := fields["id"]; !ok {
		t.Error("id not in Fields()")
	}
}

// containsStr is a test helper - checks if s contains substr
func containsStr(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || len(s) > 0 && stringContains(s, substr))
}

func stringContains(s, substr string) bool {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}
