package handlers

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
	"github.com/gin-gonic/gin"
)

// newCheckpointsHandler is a test helper that constructs a CheckpointsHandler
// backed by the sqlmock DB set up by setupTestDB.
func newCheckpointsHandler(t *testing.T, mock sqlmock.Sqlmock) *CheckpointsHandler {
	t.Helper()
	_ = mock // surfaced for callers that need to set expectations
	return NewCheckpointsHandler(db.DB)
}

// ---------- Upsert ----------

// TestCheckpointsUpsert_CreatesNew verifies that a valid POST inserts a new
// checkpoint row and returns 201 with the generated id.
func TestCheckpointsUpsert_CreatesNew(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	mock.ExpectQuery("INSERT INTO workflow_checkpoints").
		WithArgs("ws-1", "wf-abc", "step-init", 0, "null").
		WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("ckpt-001"))

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-1"}}
	body := `{"workflow_id":"wf-abc","step_name":"step-init","step_index":0}`
	c.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(body))
	c.Request.Header.Set("Content-Type", "application/json")

	h.Upsert(c)

	if w.Code != http.StatusCreated {
		t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
	}
	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["id"] != "ckpt-001" {
		t.Errorf("expected id 'ckpt-001', got %v", resp["id"])
	}
	if resp["workflow_id"] != "wf-abc" {
		t.Errorf("expected workflow_id 'wf-abc', got %v", resp["workflow_id"])
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestCheckpointsUpsert_UpdatesExisting verifies that re-POSTing the same
// (workspace_id, workflow_id, step_name) triple updates the existing row via
// ON CONFLICT DO UPDATE and still returns 201.
func TestCheckpointsUpsert_UpdatesExisting(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	// ON CONFLICT DO UPDATE — same SQL, returns existing id.
	mock.ExpectQuery("INSERT INTO workflow_checkpoints").
		WithArgs("ws-1", "wf-abc", "step-init", 2, "null").
		WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("ckpt-001"))

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-1"}}
	body := `{"workflow_id":"wf-abc","step_name":"step-init","step_index":2}`
	c.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(body))
	c.Request.Header.Set("Content-Type", "application/json")

	h.Upsert(c)

	if w.Code != http.StatusCreated {
		t.Fatalf("expected 201 on update, got %d: %s", w.Code, w.Body.String())
	}
	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["id"] != "ckpt-001" {
		t.Errorf("expected existing id 'ckpt-001', got %v", resp["id"])
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestCheckpointsUpsert_WithPayload verifies that a non-empty payload is
// forwarded to the DB as-is (stringified JSONB).
func TestCheckpointsUpsert_WithPayload(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	mock.ExpectQuery("INSERT INTO workflow_checkpoints").
		WithArgs("ws-2", "wf-xyz", "step-process", 1, `{"result":"ok"}`).
		WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("ckpt-002"))

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-2"}}
	body := `{"workflow_id":"wf-xyz","step_name":"step-process","step_index":1,"payload":{"result":"ok"}}`
	c.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(body))
	c.Request.Header.Set("Content-Type", "application/json")

	h.Upsert(c)

	if w.Code != http.StatusCreated {
		t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// ---------- List ----------

// TestCheckpointsList_OrderedByStepIndex verifies that List returns rows
// ordered by step_index DESC (highest step first, as the DB provides).
func TestCheckpointsList_OrderedByStepIndex(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	cols := []string{"id", "workspace_id", "workflow_id", "step_name", "step_index", "completed_at", "payload"}
	mock.ExpectQuery("SELECT id, workspace_id, workflow_id, step_name, step_index").
		WithArgs("ws-1", "wf-abc").
		WillReturnRows(sqlmock.NewRows(cols).
			AddRow("ckpt-b", "ws-1", "wf-abc", "step-two", 2, "2026-04-17T10:01:00Z", nil).
			AddRow("ckpt-a", "ws-1", "wf-abc", "step-one", 1, "2026-04-17T10:00:00Z", nil))

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "wfid", Value: "wf-abc"}}
	c.Request = httptest.NewRequest("GET", "/", nil)

	h.List(c)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}
	var result []map[string]interface{}
	if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
		t.Fatalf("response is not valid JSON: %v", err)
	}
	if len(result) != 2 {
		t.Fatalf("expected 2 checkpoints, got %d", len(result))
	}
	// DB returns pre-ordered (step_index DESC); first entry must be step 2.
	if result[0]["step_name"] != "step-two" {
		t.Errorf("expected step-two first (step_index=2), got %v", result[0]["step_name"])
	}
	if result[1]["step_name"] != "step-one" {
		t.Errorf("expected step-one second (step_index=1), got %v", result[1]["step_name"])
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestCheckpointsList_NotFound verifies that List returns 404 when no
// checkpoints exist for the given workflow.
func TestCheckpointsList_NotFound(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	cols := []string{"id", "workspace_id", "workflow_id", "step_name", "step_index", "completed_at", "payload"}
	mock.ExpectQuery("SELECT id, workspace_id, workflow_id, step_name, step_index").
		WithArgs("ws-1", "wf-missing").
		WillReturnRows(sqlmock.NewRows(cols)) // empty

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "wfid", Value: "wf-missing"}}
	c.Request = httptest.NewRequest("GET", "/", nil)

	h.List(c)

	if w.Code != http.StatusNotFound {
		t.Errorf("expected 404 for unknown workflow, got %d: %s", w.Code, w.Body.String())
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestCheckpointsList_RowsErr_Returns500 verifies that a rows.Err() set on
// the very first rows.Next() call causes the handler to return 500 rather
// than an empty 404.
//
// RowError(0, ...) fires on the first advance — rows.Next() returns false
// immediately with the injected error, rows.Err() is non-nil, and the
// handler must detect it and return 500. This exercises the rows.Err()
// guard that lives after the scan loop.
func TestCheckpointsList_RowsErr_Returns500(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	cols := []string{"id", "workspace_id", "workflow_id", "step_name", "step_index", "completed_at", "payload"}
	// RowError(0, err) requires a real row at index 0 to be reachable —
	// sqlmock only invokes nextErr[N] when r.pos-1 == N and the row exists.
	// The driver copies row data into dest and THEN returns the error, so
	// database/sql's rows.Next() receives a non-EOF error, sets lasterr, and
	// returns false without ever calling Scan. rows.Err() then exposes lasterr.
	mock.ExpectQuery("SELECT id, workspace_id, workflow_id, step_name, step_index").
		WithArgs("ws-1", "wf-err").
		WillReturnRows(sqlmock.NewRows(cols).
			AddRow("ckpt-ok", "ws-1", "wf-err", "step-a", 0, "2026-04-17T10:00:00Z", nil).
			RowError(0, errors.New("storage engine fault")))

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "wfid", Value: "wf-err"}}
	c.Request = httptest.NewRequest("GET", "/", nil)

	h.List(c)

	if w.Code != http.StatusInternalServerError {
		t.Errorf("rows.Err() must yield 500, got %d: %s", w.Code, w.Body.String())
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// ---------- Delete ----------

// TestCheckpointsDelete_Success verifies that DELETE returns 200 and the
// count of removed rows when checkpoints exist.
func TestCheckpointsDelete_Success(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	mock.ExpectExec("DELETE FROM workflow_checkpoints").
		WithArgs("ws-1", "wf-abc").
		WillReturnResult(sqlmock.NewResult(0, 3))

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "wfid", Value: "wf-abc"}}
	c.Request = httptest.NewRequest("DELETE", "/", nil)

	h.Delete(c)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}
	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["workflow_id"] != "wf-abc" {
		t.Errorf("expected workflow_id 'wf-abc' in response, got %v", resp["workflow_id"])
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestCheckpointsDelete_NotFound verifies that DELETE returns 404 when no
// checkpoints exist for the workflow (clean-up of already-clean workflow).
func TestCheckpointsDelete_NotFound(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	mock.ExpectExec("DELETE FROM workflow_checkpoints").
		WithArgs("ws-1", "wf-gone").
		WillReturnResult(sqlmock.NewResult(0, 0))

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-1"}, {Key: "wfid", Value: "wf-gone"}}
	c.Request = httptest.NewRequest("DELETE", "/", nil)

	h.Delete(c)

	if w.Code != http.StatusNotFound {
		t.Errorf("expected 404 for missing workflow, got %d: %s", w.Code, w.Body.String())
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// ---------- Access control (caller_workspace_id mismatch → 403) ----------

// TestCheckpointsUpsert_CallerMismatch_Returns403 verifies that Upsert
// returns 403 when the Gin context carries a caller_workspace_id that does
// not match the URL :id param. This simulates the defence-in-depth check
// that future middleware (or tests) can activate by setting the context key.
func TestCheckpointsUpsert_CallerMismatch_Returns403(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)
	// No DB expectations — handler must abort before touching the DB.

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-target"}}
	c.Set("caller_workspace_id", "ws-attacker")
	body := `{"workflow_id":"wf-x","step_name":"step-x","step_index":0}`
	c.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(body))
	c.Request.Header.Set("Content-Type", "application/json")

	h.Upsert(c)

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 on workspace mismatch, got %d: %s", w.Code, w.Body.String())
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unexpected DB calls after caller mismatch: %v", err)
	}
}

// TestCheckpointsList_CallerMismatch_Returns403 mirrors the Upsert test for
// the List endpoint.
func TestCheckpointsList_CallerMismatch_Returns403(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-target"}, {Key: "wfid", Value: "wf-x"}}
	c.Set("caller_workspace_id", "ws-attacker")
	c.Request = httptest.NewRequest("GET", "/", nil)

	h.List(c)

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 on workspace mismatch, got %d: %s", w.Code, w.Body.String())
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unexpected DB calls after caller mismatch: %v", err)
	}
}

// TestCheckpointsDelete_CallerMismatch_Returns403 mirrors the Upsert test for
// the Delete endpoint.
func TestCheckpointsDelete_CallerMismatch_Returns403(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-target"}, {Key: "wfid", Value: "wf-x"}}
	c.Set("caller_workspace_id", "ws-attacker")
	c.Request = httptest.NewRequest("DELETE", "/", nil)

	h.Delete(c)

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 on workspace mismatch, got %d: %s", w.Code, w.Body.String())
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unexpected DB calls after caller mismatch: %v", err)
	}
}

// ---------- Latest ----------

// TestCheckpointsLatest_ReturnsNewest verifies that Latest returns the most
// recently completed checkpoint (highest completed_at) for the workspace.
func TestCheckpointsLatest_ReturnsNewest(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	mock.ExpectQuery("SELECT id, workspace_id, workflow_id, step_name, step_index, completed_at, payload").
		WithArgs("ws-latest").
		WillReturnRows(
			sqlmock.NewRows([]string{
				"id", "workspace_id", "workflow_id",
				"step_name", "step_index", "completed_at", "payload",
			}).AddRow(
				"ckpt-abc", "ws-latest", "wf-123",
				"llm_call", 1, "2026-04-18T02:00:00Z", []byte(`{"success":true}`),
			),
		)

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-latest"}}
	c.Request = httptest.NewRequest("GET", "/", nil)

	h.Latest(c)

	if w.Code != http.StatusOK {
		t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
	}

	var resp map[string]interface{}
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("invalid JSON response: %v", err)
	}
	if resp["id"] != "ckpt-abc" {
		t.Errorf("expected id=ckpt-abc, got %v", resp["id"])
	}
	if resp["step_name"] != "llm_call" {
		t.Errorf("expected step_name=llm_call, got %v", resp["step_name"])
	}
	if resp["workflow_id"] != "wf-123" {
		t.Errorf("expected workflow_id=wf-123, got %v", resp["workflow_id"])
	}

	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestCheckpointsLatest_DBError_Returns500 verifies that Latest returns 500
// when the DB query itself fails (e.g., connection error, not a missing row).
func TestCheckpointsLatest_DBError_Returns500(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	mock.ExpectQuery("SELECT id, workspace_id, workflow_id, step_name, step_index, completed_at, payload").
		WithArgs("ws-err").
		WillReturnError(errors.New("db: connection refused"))

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-err"}}
	c.Request = httptest.NewRequest("GET", "/", nil)

	h.Latest(c)

	if w.Code != http.StatusInternalServerError {
		t.Errorf("expected 500 on DB error, got %d: %s", w.Code, w.Body.String())
	}

	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestCheckpointsLatest_ErrNoRows_Returns404 uses sql.ErrNoRows directly to
// verify the 404 branch is exercised.
func TestCheckpointsLatest_ErrNoRows_Returns404(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	mock.ExpectQuery("SELECT id, workspace_id, workflow_id, step_name, step_index, completed_at, payload").
		WithArgs("ws-none").
		WillReturnRows(sqlmock.NewRows([]string{
			"id", "workspace_id", "workflow_id",
			"step_name", "step_index", "completed_at", "payload",
		})) // empty result set → sql.ErrNoRows on Scan

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-none"}}
	c.Request = httptest.NewRequest("GET", "/", nil)

	h.Latest(c)

	if w.Code != http.StatusNotFound {
		t.Errorf("expected 404 for empty result, got %d: %s", w.Code, w.Body.String())
	}

	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unmet sqlmock expectations: %v", err)
	}
}

// TestCheckpointsLatest_CallerMismatch_Returns403 mirrors the Upsert test
// for the Latest endpoint.
func TestCheckpointsLatest_CallerMismatch_Returns403(t *testing.T) {
	mock := setupTestDB(t)
	h := newCheckpointsHandler(t, mock)

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Params = gin.Params{{Key: "id", Value: "ws-target"}}
	c.Set("caller_workspace_id", "ws-attacker")
	c.Request = httptest.NewRequest("GET", "/", nil)

	h.Latest(c)

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 on workspace mismatch, got %d: %s", w.Code, w.Body.String())
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("unexpected DB calls after caller mismatch: %v", err)
	}
}
