package projectsync

import (
	"errors"
	"testing"
	"time"

	"github.com/nomadcode/nomadcode-core/internal/db"
	"github.com/nomadcode/nomadcode-core/internal/workitem"
)

func TestConfigNormalizeRequiresProjectSyncFields(t *testing.T) {
	input := Config{
		Target: ProviderProjectTarget{
			Provider: workitem.ProviderID(" plane "),
			Tenant:   " general ",
			Project:  " project-1 ",
		},
		GitRemoteURL:      " git@example.com:nomadcode.git ",
		SourceBranch:      " develop ",
		WorkspaceID:       " main ",
		WorkspaceBasePath: " /home/user/workspace/ ",
		RepoDirName:       " nomadcode ",
	}

	got, err := input.Normalize()
	if err != nil {
		t.Fatalf("Normalize returned error: %v", err)
	}

	if got.Target.Provider != "plane" {
		t.Errorf("provider: got %q", got.Target.Provider)
	}
	if got.Target.Tenant != "general" {
		t.Errorf("tenant: got %q", got.Target.Tenant)
	}
	if got.Target.Project != "project-1" {
		t.Errorf("project: got %q", got.Target.Project)
	}
	if got.GitRemoteURL != "git@example.com:nomadcode.git" {
		t.Errorf("git remote: got %q", got.GitRemoteURL)
	}
	if got.SourceBranch != "develop" {
		t.Errorf("source branch: got %q", got.SourceBranch)
	}
	if got.WorkspaceID != "main" {
		t.Errorf("workspace id: got %q", got.WorkspaceID)
	}
	if got.WorkspaceBasePath != "/home/user/workspace" {
		t.Errorf("workspace base path: got %q", got.WorkspaceBasePath)
	}
	if got.RepoDirName != "nomadcode" {
		t.Errorf("repo dir name: got %q", got.RepoDirName)
	}
}

func TestConfigNormalizeRejectsMissingRequiredFields(t *testing.T) {
	valid := Config{
		Target: ProviderProjectTarget{
			Provider: "plane",
			Tenant:   "general",
			Project:  "project-1",
		},
		GitRemoteURL:      "git@example.com:nomadcode.git",
		SourceBranch:      "develop",
		WorkspaceID:       "main",
		WorkspaceBasePath: "/home/user/workspace",
		RepoDirName:       "nomadcode",
	}

	tests := map[string]Config{
		"provider":            func() Config { c := valid; c.Target.Provider = ""; return c }(),
		"tenant":              func() Config { c := valid; c.Target.Tenant = ""; return c }(),
		"project":             func() Config { c := valid; c.Target.Project = ""; return c }(),
		"git remote":          func() Config { c := valid; c.GitRemoteURL = ""; return c }(),
		"source branch":       func() Config { c := valid; c.SourceBranch = ""; return c }(),
		"workspace id":        func() Config { c := valid; c.WorkspaceID = ""; return c }(),
		"workspace base path": func() Config { c := valid; c.WorkspaceBasePath = ""; return c }(),
		"repo dir name":       func() Config { c := valid; c.RepoDirName = ""; return c }(),
	}

	for name, input := range tests {
		t.Run(name, func(t *testing.T) {
			_, err := input.Normalize()
			if !errors.Is(err, ErrInvalidConfig) {
				t.Fatalf("Normalize error: got %v, want ErrInvalidConfig", err)
			}
		})
	}
}

func TestWorkspacePathsUseRepoRootAndZeroPaddedSlot(t *testing.T) {
	root, err := ProjectWorkspaceRoot("/home/user/workspace", "nomadcode")
	if err != nil {
		t.Fatalf("ProjectWorkspaceRoot returned error: %v", err)
	}
	if root != "/home/user/workspace/nomadcode" {
		t.Fatalf("root: got %q", root)
	}

	slotPath, err := SlotWorkspacePath("/home/user/workspace", "nomadcode", DefaultSlotIndex)
	if err != nil {
		t.Fatalf("SlotWorkspacePath returned error: %v", err)
	}
	if slotPath != "/home/user/workspace/nomadcode/slots/000" {
		t.Fatalf("slot path: got %q", slotPath)
	}
}

func TestSlotIndexFormattingAndBounds(t *testing.T) {
	slot, err := NewSlotIndex(2)
	if err != nil {
		t.Fatalf("NewSlotIndex returned error: %v", err)
	}
	if slot.String() != "002" {
		t.Fatalf("slot string: got %q", slot.String())
	}

	for _, index := range []int{-1, 1000} {
		t.Run("invalid", func(t *testing.T) {
			if _, err := NewSlotIndex(index); !errors.Is(err, ErrInvalidConfig) {
				t.Fatalf("NewSlotIndex(%d) error: got %v, want ErrInvalidConfig", index, err)
			}
		})
	}
}

func TestSlotStateValidationAndAllocation(t *testing.T) {
	if !SlotStateAvailable.Valid() || !SlotStateInUse.Valid() || !SlotStateDirty.Valid() || !SlotStateError.Valid() {
		t.Fatal("expected known slot states to be valid")
	}
	if !SlotStateAvailable.Allocatable() {
		t.Fatal("available slot should be allocatable")
	}
	if SlotStateInUse.Allocatable() || SlotStateDirty.Allocatable() || SlotStateError.Allocatable() {
		t.Fatal("only available slots should be allocatable")
	}
	if SlotState("unknown").Valid() {
		t.Fatal("unknown slot state should be invalid")
	}
}

func TestRepoDirNameRejectsNestedOrAbsolutePaths(t *testing.T) {
	for _, repoDirName := range []string{"/nomadcode", "../nomadcode", "team/nomadcode", `team\nomadcode`} {
		t.Run(repoDirName, func(t *testing.T) {
			_, err := ProjectWorkspaceRoot("/home/user/workspace", repoDirName)
			if !errors.Is(err, ErrInvalidConfig) {
				t.Fatalf("ProjectWorkspaceRoot error: got %v, want ErrInvalidConfig", err)
			}
		})
	}
}

func TestSlotFromDBRecordPreservesTypedFields(t *testing.T) {
	now := time.Now()
	record := db.WorkspaceSlot{
		ID:                   11,
		ProjectSyncSettingID: 5,
		SlotIndex:            2,
		State:                string(SlotStateInUse),
		Path:                 "/home/user/workspace/nomadcode/slots/002",
		CreatedAt:            now,
		UpdatedAt:            now,
	}

	slot := SlotFromDBRecord(record)
	if slot.ID != 11 || slot.ProjectSyncSettingID != 5 {
		t.Errorf("identity not preserved: %+v", slot)
	}
	if slot.Index != SlotIndex(2) {
		t.Errorf("index: got %d", slot.Index)
	}
	if slot.State != SlotStateInUse {
		t.Errorf("state: got %q", slot.State)
	}
	if slot.Path != "/home/user/workspace/nomadcode/slots/002" {
		t.Errorf("path: got %q", slot.Path)
	}
}

func TestToUpsertWorkspaceSlotParamsValidatesIndex(t *testing.T) {
	params, err := ToUpsertWorkspaceSlotParams(5, DefaultSlotIndex, "/ws/nomadcode/slots/000")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if params.ProjectSyncSettingID != 5 || params.SlotIndex != 0 || params.Path != "/ws/nomadcode/slots/000" {
		t.Errorf("unexpected params: %+v", params)
	}

	for _, index := range []SlotIndex{-1, MaxSlotIndex + 1} {
		if _, err := ToUpsertWorkspaceSlotParams(5, index, "/ws/x"); !errors.Is(err, ErrInvalidConfig) {
			t.Errorf("index %d: expected ErrInvalidConfig, got %v", index, err)
		}
	}
}

func TestToUpdateWorkspaceSlotStateParamsAcceptsKnownStatesOnly(t *testing.T) {
	for _, state := range []SlotState{SlotStateAvailable, SlotStateInUse, SlotStateDirty, SlotStateError} {
		params, err := ToUpdateWorkspaceSlotStateParams(9, state)
		if err != nil {
			t.Fatalf("state %q: unexpected error: %v", state, err)
		}
		if params.ID != 9 || params.State != string(state) {
			t.Errorf("state %q: unexpected params: %+v", state, params)
		}
	}

	if _, err := ToUpdateWorkspaceSlotStateParams(9, SlotState("bogus")); !errors.Is(err, ErrInvalidConfig) {
		t.Errorf("expected ErrInvalidConfig for unknown state, got %v", err)
	}
}

// TestConfigToCreateProjectSyncParams verifies the DB params conversion.
func TestConfigToCreateProjectSyncParams(t *testing.T) {
	config := Config{
		Target: ProviderProjectTarget{
			Provider: "plane",
			Tenant:   "general",
			Project:  "project-x",
		},
		GitRemoteURL:      "git@gh.com:org/repo.git",
		SourceBranch:      "main",
		WorkspaceID:       "ws-abc",
		WorkspaceBasePath: "/data/ws",
		RepoDirName:       "repo",
	}

	// Normalize first (required by contract)
	normal, err := config.Normalize()
	if err != nil {
		t.Fatalf("Normalize failed: %v", err)
	}

	params := normal.ToCreateProjectSyncParams()
	if params.Provider != "plane" {
		t.Errorf("Provider: got %q", params.Provider)
	}
	if params.Tenant != "general" {
		t.Errorf("Tenant: got %q", params.Tenant)
	}
	if params.Project != "project-x" {
		t.Errorf("Project: got %q", params.Project)
	}
	if params.GitRemoteUrl != "git@gh.com:org/repo.git" {
		t.Errorf("GitRemoteUrl: got %q", params.GitRemoteUrl)
	}
	if params.SourceBranch != "main" {
		t.Errorf("SourceBranch: got %q", params.SourceBranch)
	}
	if params.WorkspaceID != "ws-abc" {
		t.Errorf("WorkspaceID: got %q", params.WorkspaceID)
	}
	if params.WorkspaceBasePath != "/data/ws" {
		t.Errorf("WorkspaceBasePath: got %q", params.WorkspaceBasePath)
	}
	if params.RepoDirName != "repo" {
		t.Errorf("RepoDirName: got %q", params.RepoDirName)
	}
}

// TestConfigFromDBRecord verifies the DB record → Config conversion.
func TestConfigFromDBRecord(t *testing.T) {
	now := time.Now()
	record := db.ProjectSyncSetting{
		ID:                7,
		Provider:          "plane",
		Tenant:            "org2",
		Project:           "proj-y",
		GitRemoteUrl:      "git@bitbucket.com:team/app.git",
		SourceBranch:      "develop",
		WorkspaceID:       "ws-def",
		WorkspaceBasePath: "/srv/ws",
		RepoDirName:       "app",
		Active:            true,
		CreatedAt:         now,
		UpdatedAt:         now,
	}

	cfg := ConfigFromDBRecord(record)
	if cfg.Target.Provider != workitem.ProviderID("plane") {
		t.Errorf("Target.Provider: got %q", cfg.Target.Provider)
	}
	if cfg.Target.Tenant != "org2" {
		t.Errorf("Target.Tenant: got %q", cfg.Target.Tenant)
	}
	if cfg.Target.Project != "proj-y" {
		t.Errorf("Target.Project: got %q", cfg.Target.Project)
	}
	if cfg.GitRemoteURL != "git@bitbucket.com:team/app.git" {
		t.Errorf("GitRemoteURL: got %q", cfg.GitRemoteURL)
	}
	if cfg.SourceBranch != "develop" {
		t.Errorf("SourceBranch: got %q", cfg.SourceBranch)
	}
	if cfg.WorkspaceID != "ws-def" {
		t.Errorf("WorkspaceID: got %q", cfg.WorkspaceID)
	}
	if cfg.WorkspaceBasePath != "/srv/ws" {
		t.Errorf("WorkspaceBasePath: got %q", cfg.WorkspaceBasePath)
	}
	if cfg.RepoDirName != "app" {
		t.Errorf("RepoDirName: got %q", cfg.RepoDirName)
	}
}

func TestBranchWorkspacePathsUseReservedBranches(t *testing.T) {
	tests := []struct {
		branch   BranchName
		wantPath string
	}{
		{BranchNameMain, "/home/user/workspace/nomadcode/branches/main"},
		{BranchNameDevelop, "/home/user/workspace/nomadcode/branches/develop"},
	}
	for _, tc := range tests {
		path, err := BranchWorkspacePath("/home/user/workspace", "nomadcode", tc.branch)
		if err != nil {
			t.Fatalf("BranchWorkspacePath(%q): unexpected error: %v", tc.branch, err)
		}
		if path != tc.wantPath {
			t.Errorf("BranchWorkspacePath(%q): got %q, want %q", tc.branch, path, tc.wantPath)
		}
	}

	// Non-reserved branch names must be rejected.
	for _, bad := range []BranchName{"feature/x", "release", "main-fork", ""} {
		if _, err := BranchWorkspacePath("/home/user/workspace", "nomadcode", bad); !errors.Is(err, ErrInvalidConfig) {
			t.Errorf("BranchWorkspacePath(%q): expected ErrInvalidConfig, got %v", bad, err)
		}
	}

	// Branch path must not overlap with slot path.
	slotPath, _ := SlotWorkspacePath("/home/user/workspace", "nomadcode", DefaultSlotIndex)
	branchPath, _ := BranchWorkspacePath("/home/user/workspace", "nomadcode", BranchNameDevelop)
	if slotPath == branchPath {
		t.Errorf("branch and slot paths must not collide: both %q", slotPath)
	}
}

func TestBranchSyncStateValidation(t *testing.T) {
	valid := []BranchSyncState{BranchSyncStateSynced, BranchSyncStateStale, BranchSyncStateError}
	for _, s := range valid {
		if !s.Valid() {
			t.Errorf("state %q should be valid", s)
		}
	}
	if BranchSyncState("unknown").Valid() {
		t.Error("unknown branch sync state should be invalid")
	}
}

func TestRequiredBranchWorkspacesContainsMainAndDevelop(t *testing.T) {
	required := RequiredBranchWorkspaces()
	found := map[BranchName]bool{}
	for _, b := range required {
		found[b] = true
	}
	if !found[BranchNameMain] {
		t.Error("RequiredBranchWorkspaces must include main")
	}
	if !found[BranchNameDevelop] {
		t.Error("RequiredBranchWorkspaces must include develop")
	}
}

// TestConfigRoundTrip verifies Config → DB params → Config preserves fields.
func TestConfigRoundTrip(t *testing.T) {
	original := Config{
		Target: ProviderProjectTarget{
			Provider: "jira",
			Tenant:   "acme",
			Project:  "board-42",
		},
		GitRemoteURL:      "git@github.com:acme/core.git",
		SourceBranch:      "release/v1",
		WorkspaceID:       "ws-rt",
		WorkspaceBasePath: "/mnt/ws",
		RepoDirName:       "core",
	}

	normal, err := original.Normalize()
	if err != nil {
		t.Fatalf("Normalize: %v", err)
	}

	params := normal.ToCreateProjectSyncParams()
	record := db.ProjectSyncSetting{
		ID:                99,
		Provider:          params.Provider,
		Tenant:            params.Tenant,
		Project:           params.Project,
		GitRemoteUrl:      params.GitRemoteUrl,
		SourceBranch:      params.SourceBranch,
		WorkspaceID:       params.WorkspaceID,
		WorkspaceBasePath: params.WorkspaceBasePath,
		RepoDirName:       params.RepoDirName,
		Active:            true,
	}

	recovered := ConfigFromDBRecord(record)
	if recovered.Target.Provider != normal.Target.Provider {
		t.Errorf("Provider round-trip: got %q", recovered.Target.Provider)
	}
	if recovered.GitRemoteURL != normal.GitRemoteURL {
		t.Errorf("GitRemoteURL round-trip: got %q", recovered.GitRemoteURL)
	}
}
