package diff

import (
	"context"
	"errors"
	"testing"

	"github.com/open-code-review/open-code-review/internal/config/template"
	"github.com/open-code-review/open-code-review/internal/llm"
	"github.com/open-code-review/open-code-review/internal/model"
)

type mockLLMClient struct {
	response *llm.ChatResponse
	err      error
}

func (m *mockLLMClient) CompletionsWithCtx(_ context.Context, req llm.ChatRequest) (*llm.ChatResponse, error) {
	return m.response, m.err
}

func newMockResponse(content string) *llm.ChatResponse {
	return &llm.ChatResponse{
		Choices: []llm.Choice{
			{Message: llm.ResponseMessage{Role: "assistant", Content: &content}},
		},
	}
}

func makeTask() *template.LlmConversation {
	return &template.LlmConversation{
		Timeout: 60,
		Messages: []template.ChatMessage{
			{Role: "system", Content: "you are a helper"},
			{Role: "user", Content: "diff:\n{diff}\n\ncomment:\n{suggestion_content}"},
		},
	}
}

func makeDiff() *model.Diff {
	return &model.Diff{
		NewPath: "main.go",
		Diff: `@@ -10,6 +10,8 @@
 import "fmt"

 func main() {
+    x := 1
+    y := 2
     fmt.Println("hello")
 }
`,
	}
}

func TestResolveComment_TextMatchSuccess(t *testing.T) {
	cm := model.LlmComment{
		Path:         "main.go",
		Content:      "unused variable",
		ExistingCode: "x := 1\ny := 2",
	}
	d := makeDiff()

	ok := ResolveComment(&cm, d)
	if !ok {
		t.Fatal("expected ResolveComment to succeed")
	}
	if cm.StartLine == 0 || cm.EndLine == 0 {
		t.Fatalf("expected non-zero lines, got %d-%d", cm.StartLine, cm.EndLine)
	}
}

func TestResolveComment_AlreadyResolved(t *testing.T) {
	cm := model.LlmComment{
		Path:         "main.go",
		Content:      "test",
		ExistingCode: "whatever",
		StartLine:    5,
		EndLine:      10,
	}
	d := makeDiff()
	ok := ResolveComment(&cm, d)
	if !ok {
		t.Fatal("expected true for already-resolved comment")
	}
	if cm.StartLine != 5 || cm.EndLine != 10 {
		t.Fatal("should not change already-resolved lines")
	}
}

func TestResolveComment_EmptyExistingCode(t *testing.T) {
	cm := model.LlmComment{Path: "main.go", Content: "test"}
	d := makeDiff()
	ok := ResolveComment(&cm, d)
	if ok {
		t.Fatal("expected false for empty ExistingCode")
	}
}

func TestReLocateComment_LLMReturnsValidCode(t *testing.T) {
	cm := model.LlmComment{
		Path:         "main.go",
		Content:      "unused variable",
		ExistingCode: "totally wrong code that won't match",
	}
	d := makeDiff()

	client := &mockLLMClient{
		response: newMockResponse("Here is the code:\n```go\nx := 1\ny := 2\n```\n"),
	}

	ok, resp, msgs := ReLocateComment(context.Background(), &cm, d, client, makeTask(), "test-model", 1000)
	if !ok {
		t.Fatal("expected re-location to succeed")
	}
	if resp == nil {
		t.Fatal("expected non-nil response")
	}
	if len(msgs) == 0 {
		t.Fatal("expected non-empty messages")
	}
	if cm.StartLine == 0 || cm.EndLine == 0 {
		t.Fatalf("expected non-zero lines after re-location, got %d-%d", cm.StartLine, cm.EndLine)
	}
}

func TestReLocateComment_LLMReturnsInvalidContent(t *testing.T) {
	cm := model.LlmComment{
		Path:         "main.go",
		Content:      "unused variable",
		ExistingCode: "totally wrong code",
	}
	d := makeDiff()

	client := &mockLLMClient{
		response: newMockResponse("I cannot find the code."),
	}

	ok, resp, msgs := ReLocateComment(context.Background(), &cm, d, client, makeTask(), "test-model", 1000)
	if ok {
		t.Fatal("expected re-location to fail for invalid LLM response")
	}
	if resp == nil {
		t.Fatal("expected non-nil response even on failure")
	}
	if len(msgs) == 0 {
		t.Fatal("expected non-empty messages")
	}
	if cm.StartLine != 0 || cm.EndLine != 0 {
		t.Fatal("lines should remain 0-0")
	}
}

func TestReLocateComment_LLMError(t *testing.T) {
	cm := model.LlmComment{
		Path:         "main.go",
		Content:      "test",
		ExistingCode: "bad code",
	}
	d := makeDiff()

	client := &mockLLMClient{err: errors.New("network error")}

	ok, resp, msgs := ReLocateComment(context.Background(), &cm, d, client, makeTask(), "test-model", 1000)
	if ok {
		t.Fatal("expected false on LLM error")
	}
	if resp != nil {
		t.Fatal("expected nil response on error")
	}
	if len(msgs) == 0 {
		t.Fatal("expected non-empty messages even on error")
	}
}

func TestReLocateComment_NilTask(t *testing.T) {
	cm := model.LlmComment{
		Path:         "main.go",
		Content:      "test",
		ExistingCode: "bad code",
	}
	d := makeDiff()
	client := &mockLLMClient{}

	ok, resp, msgs := ReLocateComment(context.Background(), &cm, d, client, nil, "test-model", 1000)
	if ok {
		t.Fatal("expected false when task is nil")
	}
	if resp != nil {
		t.Fatal("expected nil response when task is nil")
	}
	if msgs != nil {
		t.Fatal("expected nil messages when task is nil")
	}
}

func TestExtractCodeBlock(t *testing.T) {
	tests := []struct {
		name  string
		input string
		want  string
	}{
		{"with language tag", "```go\nfoo\nbar\n```", "foo\nbar"},
		{"without language tag", "```\nfoo\n```", "foo"},
		{"with surrounding text", "Here:\n```\ncode\n```\ndone", "code"},
		{"no code block", "just text", ""},
		{"empty block", "```\n```", ""},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := extractCodeBlock(tt.input)
			if got != tt.want {
				t.Errorf("extractCodeBlock() = %q, want %q", got, tt.want)
			}
		})
	}
}
