package pool

import (
	"encoding/json"
	"strings"
	"testing"
	"time"

	"crypto-miner-server/internal/models"
)

// ─── parseAndSetJob ───────────────────────────────────────────────────────────

func newTestProxy() *Proxy {
	return NewProxy(&Config{
		Host:     "pool.test",
		Port:     3333,
		Wallet:   "89QUKeqsKEGfP9Vpiph8jEXc3YyVFN5dKeYdMFVraVG4SGU3jAprbBp9AgRutKxzPSdQQMp9EGeG7Wmh8NRfniiaMMYpmC3",
		Password: "x",
	})
}

func TestParseAndSetJobDirectObject(t *testing.T) {
	p := newTestProxy()
	var received *Job
	p.SetCallbacks(func(j *Job) { received = j }, nil, nil)

	data := json.RawMessage(`{
		"job_id":"abc123","height":3688000,"blob":"0c0caffee",
		"target":"ffffffff","seed_hash":"aabbccdd","difficulty":100000,"algo":"rx/0"
	}`)
	p.parseAndSetJob(data)

	if received == nil {
		t.Fatal("onJob not called")
	}
	if received.ID != "abc123" {
		t.Errorf("ID = %q, want abc123", received.ID)
	}
	if received.Height != 3688000 {
		t.Errorf("Height = %d, want 3688000", received.Height)
	}
	if received.Algo != "rx/0" {
		t.Errorf("Algo = %q, want rx/0", received.Algo)
	}
	if received.Target != "ffffffff" {
		t.Errorf("Target = %q, want ffffffff", received.Target)
	}
}

func TestParseAndSetJobSetsCurrentJob(t *testing.T) {
	p := newTestProxy()
	data := json.RawMessage(`{"job_id":"xyz","height":100,"blob":"aa","target":"ff","seed_hash":"bb"}`)
	p.parseAndSetJob(data)

	got := p.GetCurrentJob()
	if got == nil {
		t.Fatal("GetCurrentJob() returned nil after parseAndSetJob")
	}
	if got.ID != "xyz" {
		t.Errorf("current job ID = %q, want xyz", got.ID)
	}
}

func TestParseAndSetJobTargetDerivedFromDifficulty(t *testing.T) {
	p := newTestProxy()
	var received *Job
	p.SetCallbacks(func(j *Job) { received = j }, nil, nil)

	// No target field — should be derived from difficulty.
	data := json.RawMessage(`{"job_id":"d1","height":1,"blob":"aa","seed_hash":"bb","difficulty":10000}`)
	p.parseAndSetJob(data)

	if received == nil {
		t.Fatal("onJob not called")
	}
	if received.Target == "" {
		t.Error("Target should be derived from difficulty when absent")
	}
	if len(received.Target) != 64 {
		t.Errorf("derived Target length = %d, want 64 hex chars", len(received.Target))
	}
}

func TestParseAndSetJobInvalidJSONIgnored(t *testing.T) {
	p := newTestProxy()
	called := false
	p.SetCallbacks(func(j *Job) { called = true }, nil, nil)

	p.parseAndSetJob(json.RawMessage(`this is not json`))
	if called {
		t.Error("onJob should not be called for invalid JSON")
	}
}

func TestParseAndSetJobCallbackNilSafe(t *testing.T) {
	p := newTestProxy()
	// No callbacks set — must not panic.
	data := json.RawMessage(`{"job_id":"safe","height":1,"blob":"aa","target":"ff","seed_hash":"bb"}`)
	p.parseAndSetJob(data)
}

func TestParseAndSetJobOverwritesPrevious(t *testing.T) {
	p := newTestProxy()
	p.parseAndSetJob(json.RawMessage(`{"job_id":"first","height":1,"blob":"aa","target":"ff","seed_hash":"bb"}`))
	p.parseAndSetJob(json.RawMessage(`{"job_id":"second","height":2,"blob":"cc","target":"ff","seed_hash":"bb"}`))

	got := p.GetCurrentJob()
	if got.ID != "second" {
		t.Errorf("expected latest job ID 'second', got %q", got.ID)
	}
}

// ─── difficultyToTarget ───────────────────────────────────────────────────────

func TestDifficultyToTargetLength(t *testing.T) {
	p := newTestProxy()
	target := p.difficultyToTarget(100000)
	if len(target) != 64 {
		t.Errorf("target hex length = %d, want 64", len(target))
	}
}

func TestDifficultyToTargetDiff1(t *testing.T) {
	p := newTestProxy()
	target := p.difficultyToTarget(1)
	// Difficulty 1 → max target (all f's).
	if !strings.HasPrefix(target, "ff") && target != strings.Repeat("f", 64) {
		// At diff=1 we get maxTarget, which reversed is all ff...
		// Just check it's non-zero and 64 chars long.
		t.Logf("diff=1 target: %s", target)
	}
}

func TestDifficultyToTargetHigherDiffLowerTarget(t *testing.T) {
	p := newTestProxy()
	t1 := p.difficultyToTarget(1000)
	t2 := p.difficultyToTarget(1000000)
	// Higher difficulty → lower target value in little-endian hex.
	// We can't compare directly without reversing, but they must differ.
	if t1 == t2 {
		t.Error("different difficulties should produce different targets")
	}
}

// ─── parseSubmitResult ────────────────────────────────────────────────────────

func TestParseSubmitResultNilError(t *testing.T) {
	resp := StratumResponse{ID: 1, Result: json.RawMessage(`true`)}
	accepted, msg := parseSubmitResult(resp)
	if !accepted || msg != "" {
		t.Errorf("true result: accepted=%v msg=%q", accepted, msg)
	}
}

func TestParseSubmitResultStatusOK(t *testing.T) {
	resp := StratumResponse{ID: 1, Result: json.RawMessage(`{"status":"OK"}`)}
	accepted, msg := parseSubmitResult(resp)
	if !accepted || msg != "" {
		t.Errorf("status=OK: accepted=%v msg=%q", accepted, msg)
	}
}

func TestParseSubmitResultStatusAccepted(t *testing.T) {
	resp := StratumResponse{ID: 1, Result: json.RawMessage(`{"status":"ACCEPTED"}`)}
	accepted, _ := parseSubmitResult(resp)
	if !accepted {
		t.Error("status=ACCEPTED should be accepted")
	}
}

func TestParseSubmitResultRejectedBool(t *testing.T) {
	resp := StratumResponse{ID: 1, Result: json.RawMessage(`false`)}
	accepted, _ := parseSubmitResult(resp)
	if accepted {
		t.Error("false result should be rejected")
	}
}

func TestParseSubmitResultStringError(t *testing.T) {
	resp := StratumResponse{ID: 1, Error: "low difficulty"}
	accepted, msg := parseSubmitResult(resp)
	if accepted || msg != "low difficulty" {
		t.Errorf("string error: accepted=%v msg=%q", accepted, msg)
	}
}

func TestParseSubmitResultArrayError(t *testing.T) {
	resp := StratumResponse{ID: 1, Error: []interface{}{float64(23), "invalid share"}}
	accepted, msg := parseSubmitResult(resp)
	if accepted || msg != "invalid share" {
		t.Errorf("array error: accepted=%v msg=%q", accepted, msg)
	}
}

func TestParseSubmitResultMapError(t *testing.T) {
	resp := StratumResponse{ID: 1, Error: map[string]interface{}{"message": "duplicate share"}}
	accepted, msg := parseSubmitResult(resp)
	if accepted || msg != "duplicate share" {
		t.Errorf("map error: accepted=%v msg=%q", accepted, msg)
	}
}

func TestParseSubmitResultEmptyResult(t *testing.T) {
	resp := StratumResponse{ID: 1}
	accepted, _ := parseSubmitResult(resp)
	// Empty result with no error → optimistically accepted.
	if !accepted {
		t.Error("empty result with no error should be accepted")
	}
}

// ─── ParseBlob ────────────────────────────────────────────────────────────────

func TestParseBlobValidLength(t *testing.T) {
	// 76-byte Monero-style blob (152 hex chars).
	blob := strings.Repeat("ab", 76)
	result, err := ParseBlob(blob)
	if err != nil {
		t.Fatalf("ParseBlob valid: %v", err)
	}
	if result["nonce_offset"] != 9 {
		t.Errorf("nonce_offset = %v, want 9", result["nonce_offset"])
	}
}

func TestParseBlobTooShort(t *testing.T) {
	blob := strings.Repeat("ab", 10) // 10 bytes — too short
	_, err := ParseBlob(blob)
	if err == nil {
		t.Error("expected error for blob shorter than 43 bytes")
	}
}

func TestParseBlobInvalidHex(t *testing.T) {
	_, err := ParseBlob("not-hex")
	if err == nil {
		t.Error("expected error for non-hex input")
	}
}

// ─── FromModelJob / ToModelJob ────────────────────────────────────────────────

func TestFromModelJobNil(t *testing.T) {
	if j := FromModelJob(nil); j != nil {
		t.Error("FromModelJob(nil) should return nil")
	}
}

func TestFromModelJobRoundTrip(t *testing.T) {
	mj := &models.Job{
		ID:            "job-rt",
		Height:        3500000,
		Difficulty:    999999,
		BlockTemplate: "template",
		SeedHash:      "aabbcc",
		Target:        "ffffff",
		CreatedAt:     time.Now(),
	}
	pj := FromModelJob(mj)
	if pj == nil {
		t.Fatal("FromModelJob returned nil")
	}
	if pj.ID != mj.ID || pj.Height != mj.Height || pj.Difficulty != mj.Difficulty {
		t.Errorf("FromModelJob fields mismatch: %+v", pj)
	}

	back := pj.ToModelJob()
	if back.ID != mj.ID || back.Height != mj.Height {
		t.Errorf("ToModelJob round-trip failed: %+v", back)
	}
}

func TestToModelJobSetsCreatedAt(t *testing.T) {
	before := time.Now()
	pj := &Job{ID: "ts-test", Height: 1}
	mj := pj.ToModelJob()
	if mj.CreatedAt.Before(before) {
		t.Error("ToModelJob.CreatedAt should be set to current time")
	}
}

// ─── GetCurrentJob nil guard ──────────────────────────────────────────────────

func TestGetCurrentJobNilBeforeFirst(t *testing.T) {
	p := newTestProxy()
	if j := p.GetCurrentJob(); j != nil {
		t.Errorf("expected nil before first job, got %+v", j)
	}
}
