package main

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"
	"time"
)

// withModuleKey sets KANBAN_MODULE_KEY for the duration of a test and
// restores the previous value afterwards.
func withModuleKey(t *testing.T, value string) {
	t.Helper()
	prev := os.Getenv(moduleKeyEnv)
	t.Setenv(moduleKeyEnv, value)
	t.Cleanup(func() { _ = os.Setenv(moduleKeyEnv, prev) })
}

func TestCryptoRoundTrip(t *testing.T) {
	withModuleKey(t, "test-passphrase")
	plain := []byte(`{"hello":"world"}`)
	cipherBlob, nonce, err := encryptConfig(plain)
	if err != nil {
		t.Fatalf("encrypt: %v", err)
	}
	got, err := decryptConfig(cipherBlob, nonce)
	if err != nil {
		t.Fatalf("decrypt: %v", err)
	}
	if string(got) != string(plain) {
		t.Fatalf("roundtrip mismatch: got %q want %q", got, plain)
	}
}

func TestCryptoMissingKey(t *testing.T) {
	t.Setenv(moduleKeyEnv, "")
	if _, _, err := encryptConfig([]byte("x")); err == nil {
		t.Fatal("expected error when KANBAN_MODULE_KEY unset")
	}
}

func TestSaveAndLoadModule(t *testing.T) {
	withModuleKey(t, "test-passphrase")
	db := setupTestDB(t)
	m := &Module{
		Name: "jira-test", Kind: "jira", Enabled: true,
		EventFilter: []string{"card.created", "card.moved"},
		Config: JSONValue{
			"base_url": "https://example.atlassian.net",
			"email":    "x@y.z",
			"api_token": "secret-123",
		},
	}
	if err := db.saveModule(m); err != nil {
		t.Fatalf("save: %v", err)
	}
	if m.ID == "" {
		t.Fatal("ID not assigned on insert")
	}
	got, err := db.getModule(m.ID)
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	if got.Config["api_token"] != "secret-123" {
		t.Fatalf("token roundtrip failed: %v", got.Config["api_token"])
	}
}

func TestFilterMatches(t *testing.T) {
	if !filterMatches([]string{"card.created"}, "card.created") {
		t.Fatal("exact match")
	}
	if !filterMatches([]string{"*"}, "anything") {
		t.Fatal("wildcard")
	}
	if filterMatches([]string{"card.created"}, "card.moved") {
		t.Fatal("non-match should be false")
	}
}

func TestCardOptOutTag(t *testing.T) {
	c := cardForJira{Tags: []string{"foo", "NoJira", "bar"}}
	if !c.hasTag("nojira") {
		t.Fatal("nojira (case-insensitive) not detected")
	}
	if c.hasTag("missing") {
		t.Fatal("missing tag returned true")
	}
}

func TestJiraHandler_TransitionMappingMissing(t *testing.T) {
	withModuleKey(t, "k")
	db := setupTestDB(t)
	col, _ := db.CreateColumn("Backlog")
	card, _ := db.CreateCard(col.ID, "req", "t", "d", "")
	// Link the card so the create-fallback path is skipped.
	_ = db.setCardJiraKey(card.ID, "KAN-1")
	h := &jiraHandler{}
	_, err := h.transition(context.Background(), db, jiraConfig{BaseURL: "http://x"}, Event{Type: "card.moved", CardID: card.ID})
	if err == nil || !strings.Contains(err.Error(), "status_map") {
		t.Fatalf("expected status_map error, got %v", err)
	}
}

func TestJiraHandler_TestConnectionHitsMyself(t *testing.T) {
	var path string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		path = r.URL.Path
		w.WriteHeader(http.StatusOK)
		_, _ = io.WriteString(w, `{"accountId":"abc"}`)
	}))
	defer srv.Close()
	h := &jiraHandler{}
	m := Module{Kind: "jira", Config: JSONValue{
		"base_url":  srv.URL,
		"email":     "x@y.z",
		"api_token": "tok",
	}}
	status, err := h.TestConnection(context.Background(), m)
	if err != nil {
		t.Fatalf("TestConnection: %v", err)
	}
	if status != 200 {
		t.Fatalf("status = %d, want 200", status)
	}
	if path != "/rest/api/3/myself" {
		t.Fatalf("path = %q, want /rest/api/3/myself", path)
	}
}

func TestJiraHandler_CreateLinksCardKey(t *testing.T) {
	withModuleKey(t, "test-passphrase")
	db := setupTestDB(t)
	user, _ := db.CreateUser("alice", "passw", "Alice")
	col, _ := db.CreateColumn("Todo")
	card, _ := db.CreateCard(col.ID, "req", "Buy bread", "desc", user.ID)

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodPost && r.URL.Path == "/rest/api/3/issue":
			b, _ := io.ReadAll(r.Body)
			var p struct {
				Fields struct {
					Summary string `json:"summary"`
				} `json:"fields"`
			}
			_ = json.Unmarshal(b, &p)
			if p.Fields.Summary != "Buy bread" {
				t.Errorf("summary = %q", p.Fields.Summary)
			}
			w.WriteHeader(http.StatusCreated)
			_, _ = io.WriteString(w, `{"id":"10000","key":"KAN-1"}`)
		case r.Method == http.MethodGet && r.URL.Path == "/rest/api/3/issue/KAN-1/transitions":
			w.WriteHeader(http.StatusOK)
			_, _ = io.WriteString(w, `{"transitions":[{"id":"11","name":"Start","to":{"name":"To Do"}}]}`)
		case r.Method == http.MethodPost && r.URL.Path == "/rest/api/3/issue/KAN-1/transitions":
			w.WriteHeader(http.StatusNoContent)
		case r.Method == http.MethodPut && r.URL.Path == "/rest/api/3/issue/KAN-1":
			w.WriteHeader(http.StatusNoContent)
		default:
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer srv.Close()

	h := &jiraHandler{}
	mod := Module{Kind: "jira", Config: JSONValue{
		"base_url":    srv.URL,
		"email":       "x@y.z",
		"api_token":   "tok",
		"project_key": "KAN",
		"status_map":  map[string]interface{}{"Todo": "To Do"},
	}}
	status, err := h.Handle(context.Background(), db, mod, Event{Type: "card.created", CardID: card.ID})
	if err != nil {
		t.Fatalf("Handle: %v", err)
	}
	if status != http.StatusCreated {
		t.Fatalf("status = %d, want 201", status)
	}
	again, err := db.getCardForJira(card.ID)
	if err != nil {
		t.Fatalf("get card: %v", err)
	}
	if again.JiraKey != "KAN-1" {
		t.Fatalf("jira_key = %q, want KAN-1", again.JiraKey)
	}
}

func TestDispatcher_Cutoff(t *testing.T) {
	withModuleKey(t, "k")
	db := setupTestDB(t)
	col, _ := db.CreateColumn("Todo")
	// Create card BEFORE the module so cutoffOK rejects it.
	card, _ := db.CreateCard(col.ID, "req", "t", "d", "")
	time.Sleep(20 * time.Millisecond)
	mod := Module{ID: "m", CreatedAt: nowRFC3339()}
	if cutoffOK(db, mod, Event{CardID: card.ID}) {
		t.Fatal("card pre-dating module should be filtered out")
	}
	// Once linked, cutoff should allow it.
	_ = db.setCardJiraKey(card.ID, "KAN-9")
	if !cutoffOK(db, mod, Event{CardID: card.ID}) {
		t.Fatal("linked card must pass cutoff even if older")
	}
}

func TestIsAdmin(t *testing.T) {
	db := setupTestDB(t)
	u, _ := db.CreateUser("egutierrez", "passw", "Egu")
	// Migration 015 marks egutierrez admin via UPDATE WHERE username, but
	// that only takes effect when the row already exists. In production
	// the migration runs against an existing user list; in tests we create
	// users after migration, so simulate the same outcome explicitly.
	if _, err := db.conn.Exec(`UPDATE users SET is_admin = 1 WHERE username = ?`, "egutierrez"); err != nil {
		t.Fatalf("seed admin: %v", err)
	}
	ok, err := db.IsAdmin(u.ID)
	if err != nil {
		t.Fatalf("IsAdmin: %v", err)
	}
	if !ok {
		t.Fatal("egutierrez must be admin after seed")
	}
	other, _ := db.CreateUser("alice", "passw", "Alice")
	ok, _ = db.IsAdmin(other.ID)
	if ok {
		t.Fatal("alice must not be admin by default")
	}
}
