package builtin

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestDeleteRangeBasic(t *testing.T) {
	f := filepath.Join(t.TempDir(), "a.txt")
	body := "line1\nline2\nline3\nline4\nline5\n"
	os.WriteFile(f, []byte(body), 0o644)

	out := runTool(t, deleteRange{}, map[string]any{
		"path": f, "start_anchor": "line2", "end_anchor": "line4",
	})
	if !strings.Contains(out, "---") || !strings.Contains(out, "+++") {
		t.Errorf("expected unified diff output, got: %s", out)
	}
	got, _ := os.ReadFile(f)
	want := "line1\nline5\n"
	if string(got) != want {
		t.Errorf("file = %q, want %q", got, want)
	}
}

func TestDeleteRangeInclusive(t *testing.T) {
	f := filepath.Join(t.TempDir(), "a.txt")
	os.WriteFile(f, []byte("line1\nline2\nline3\nline4\nline5\n"), 0o644)
	runTool(t, deleteRange{}, map[string]any{
		"path": f, "start_anchor": "line2", "end_anchor": "line4", "inclusive": true,
	})
	got, _ := os.ReadFile(f)
	if string(got) != "line1\nline5\n" {
		t.Errorf("inclusive=true: got %q, want %q", got, "line1\\nline5\\n")
	}

	f2 := filepath.Join(t.TempDir(), "b.txt")
	os.WriteFile(f2, []byte("line1\nline2\nline3\nline4\nline5\n"), 0o644)
	runTool(t, deleteRange{}, map[string]any{
		"path": f2, "start_anchor": "line2", "end_anchor": "line4", "inclusive": false,
	})
	got2, _ := os.ReadFile(f2)
	if string(got2) != "line1\nline2\nline4\nline5\n" {
		t.Errorf("inclusive=false: got %q, want %q", got2, "line1\\nline2\\nline4\\nline5\\n")
	}
}

func TestDeleteRangeDuplicateAnchor(t *testing.T) {
	f := filepath.Join(t.TempDir(), "dup.txt")
	body := "line1\nline2\nline3\nline2\nline5\n"
	os.WriteFile(f, []byte(body), 0o644)

	args := argsJSON(t, map[string]any{
		"path": f, "start_anchor": "line2", "end_anchor": "line5",
	})
	_, err := (deleteRange{}).Execute(context.Background(), args)
	if err == nil {
		t.Fatal("expected duplicate anchor error")
	}
	if !strings.Contains(err.Error(), "not unique") {
		t.Errorf("error should mention 'not unique': %v", err)
	}
	got, _ := os.ReadFile(f)
	if string(got) != body {
		t.Errorf("file modified despite error: %q", got)
	}
}

func TestDeleteRangeMissingAnchor(t *testing.T) {
	f := filepath.Join(t.TempDir(), "missing.txt")
	body := "line1\nline2\nline3\n"
	os.WriteFile(f, []byte(body), 0o644)

	args := argsJSON(t, map[string]any{
		"path": f, "start_anchor": "line2", "end_anchor": "no_such_line",
	})
	_, err := (deleteRange{}).Execute(context.Background(), args)
	if err == nil {
		t.Fatal("expected missing anchor error")
	}
	if !strings.Contains(err.Error(), "not found") {
		t.Errorf("error should mention 'not found': %v", err)
	}
}

func TestDeleteRangeReversed(t *testing.T) {
	f := filepath.Join(t.TempDir(), "rev.txt")
	body := "line1\nline2\nline3\nline4\nline5\n"
	os.WriteFile(f, []byte(body), 0o644)

	args := argsJSON(t, map[string]any{
		"path": f, "start_anchor": "line4", "end_anchor": "line2",
	})
	_, err := (deleteRange{}).Execute(context.Background(), args)
	if err == nil {
		t.Fatal("expected reversed anchor error")
	}
	if !strings.Contains(err.Error(), "after") {
		t.Errorf("error should mention ordering: %v", err)
	}
}

func TestDeleteRangeRejectsEndAnchorOpeningBlock(t *testing.T) {
	f := filepath.Join(t.TempDir(), "map.html")
	body := strings.Join([]string{
		"// map engine",
		"function switchToProvinceMap() {",
		"  redraw();",
		"}",
		"function highlightCurrentOnMap() {",
		"  if (!state.mapInstance) return;",
		"  repaint();",
		"}",
		"",
	}, "\n")
	os.WriteFile(f, []byte(body), 0o644)

	args := argsJSON(t, map[string]any{
		"path": f, "start_anchor": "// map engine", "end_anchor": "function highlightCurrentOnMap() {",
	})
	_, err := (deleteRange{}).Execute(context.Background(), args)
	if err == nil {
		t.Fatal("expected block-opening end_anchor to be rejected")
	}
	for _, want := range []string{"appears to open a code block", "closing line", "edit_file/multi_edit"} {
		if !strings.Contains(err.Error(), want) {
			t.Errorf("error should mention %q: %v", want, err)
		}
	}
	got, _ := os.ReadFile(f)
	if string(got) != body {
		t.Errorf("file modified despite rejected range:\n%s", got)
	}
}

func TestDeleteRangeAllowsCompleteBraceBlock(t *testing.T) {
	f := filepath.Join(t.TempDir(), "map.html")
	body := strings.Join([]string{
		"function before() {",
		"  keep();",
		"}",
		"function removeMe() {",
		"  if (ok) {",
		"    redraw();",
		"  }",
		"} // removeMe",
		"function after() {",
		"  keep();",
		"}",
		"",
	}, "\n")
	os.WriteFile(f, []byte(body), 0o644)

	runTool(t, deleteRange{}, map[string]any{
		"path": f, "start_anchor": "function removeMe() {", "end_anchor": "} // removeMe",
	})
	got, _ := os.ReadFile(f)
	want := strings.Join([]string{
		"function before() {",
		"  keep();",
		"}",
		"function after() {",
		"  keep();",
		"}",
		"",
	}, "\n")
	if string(got) != want {
		t.Errorf("file = %q, want %q", got, want)
	}
}

func TestDeleteRangeRejectsClosingBraceWithoutOpening(t *testing.T) {
	f := filepath.Join(t.TempDir(), "map.html")
	body := strings.Join([]string{
		"function keepHeader() {",
		"  removeBody();",
		"} // keepHeader",
		"function after() {",
		"  keep();",
		"}",
		"",
	}, "\n")
	os.WriteFile(f, []byte(body), 0o644)

	args := argsJSON(t, map[string]any{
		"path": f, "start_anchor": "  removeBody();", "end_anchor": "} // keepHeader",
	})
	_, err := (deleteRange{}).Execute(context.Background(), args)
	if err == nil {
		t.Fatal("expected range cutting a block close to be rejected")
	}
	for _, want := range []string{"would cut a code block", "closing brace", "opening brace"} {
		if !strings.Contains(err.Error(), want) {
			t.Errorf("error should mention %q: %v", want, err)
		}
	}
	got, _ := os.ReadFile(f)
	if string(got) != body {
		t.Errorf("file modified despite rejected range:\n%s", got)
	}
}

func TestDeleteRangeAllowsPartialBraceDeletionInPlainText(t *testing.T) {
	f := filepath.Join(t.TempDir(), "notes.md")
	body := strings.Join([]string{
		"intro",
		"example {",
		"  remove this prose line",
		"}",
		"end",
		"",
	}, "\n")
	os.WriteFile(f, []byte(body), 0o644)

	runTool(t, deleteRange{}, map[string]any{
		"path": f, "start_anchor": "example {", "end_anchor": "  remove this prose line",
	})
	got, _ := os.ReadFile(f)
	want := strings.Join([]string{
		"intro",
		"}",
		"end",
		"",
	}, "\n")
	if string(got) != want {
		t.Errorf("file = %q, want %q", got, want)
	}
}

func TestDeleteRangeDuplicateAnchorReportsLines(t *testing.T) {
	f := filepath.Join(t.TempDir(), "dup-separator.js")
	body := strings.Join([]string{
		"// ═══════════════════════════════════════",
		"const a = 1;",
		"// ═══════════════════════════════════════",
		"const b = 2;",
		"// ═══════════════════════════════════════",
		"",
	}, "\n")
	os.WriteFile(f, []byte(body), 0o644)

	args := argsJSON(t, map[string]any{
		"path": f, "start_anchor": "// ═══════════════════════════════════════", "end_anchor": "const b = 2;",
	})
	_, err := (deleteRange{}).Execute(context.Background(), args)
	if err == nil {
		t.Fatal("expected duplicate anchor error")
	}
	for _, want := range []string{"not unique", "matching lines include 1, 3, 5", "repeated separator lines"} {
		if !strings.Contains(err.Error(), want) {
			t.Errorf("error should mention %q: %v", want, err)
		}
	}
}

func TestDeleteRangeCRLF(t *testing.T) {
	f := filepath.Join(t.TempDir(), "crlf.txt")
	body := "line1\r\nline2\r\nline3\r\nline4\r\nline5\r\n"
	os.WriteFile(f, []byte(body), 0o644)

	runTool(t, deleteRange{}, map[string]any{
		"path": f, "start_anchor": "line2", "end_anchor": "line4",
	})
	got, _ := os.ReadFile(f)
	want := "line1\r\nline5\r\n"
	if string(got) != want {
		t.Errorf("CRLF file: got %q, want %q", got, want)
	}
}

func TestDeleteRangeWholeNewlineTerminatedFile(t *testing.T) {
	f := filepath.Join(t.TempDir(), "whole.txt")
	os.WriteFile(f, []byte("line1\n"), 0o644)

	runTool(t, deleteRange{}, map[string]any{
		"path": f, "start_anchor": "line1", "end_anchor": "line1",
	})
	got, _ := os.ReadFile(f)
	if string(got) != "" {
		t.Errorf("whole-file delete left content %q, want empty", got)
	}
}

func TestDeleteRangePreview(t *testing.T) {
	f := filepath.Join(t.TempDir(), "preview.txt")
	body := "line1\nline2\nline3\nline4\nline5\n"
	os.WriteFile(f, []byte(body), 0o644)

	change, err := deleteRange{}.Preview(context.Background(), argsJSON(t, map[string]any{
		"path": f, "start_anchor": "line2", "end_anchor": "line4",
	}))
	if err != nil {
		t.Fatalf("Preview: %v", err)
	}

	got, _ := os.ReadFile(f)
	if string(got) != body {
		t.Errorf("Preview mutated the file: %q", got)
	}

	if change.Kind != "modify" {
		t.Errorf("kind = %q, want modify", change.Kind)
	}
	if change.OldText != body {
		t.Errorf("OldText = %q, want %q", change.OldText, body)
	}
}

func TestDeleteRangeResolvesClosedAnchorTarget(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "target.txt")
	if err := os.WriteFile(path, []byte("before\nstart\nmiddle\nend\nafter\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	target, err := (deleteRange{workDir: dir}).ResolveAnchoredTextTarget(context.Background(), argsJSON(t, map[string]any{
		"path": "target.txt", "start_anchor": "start", "end_anchor": "end", "inclusive": false,
	}))
	if err != nil {
		t.Fatalf("ResolveAnchoredTextTarget: %v", err)
	}
	if target.Path != path || target.StartLine != 2 || target.EndLine != 4 || target.Inclusive || len(target.LineHashes) != 3 {
		t.Fatalf("target = %+v, want canonical path and closed lines 2-4", target)
	}
	for i, line := range []string{"start", "middle", "end"} {
		sum := sha256.Sum256([]byte(line))
		if target.LineHashes[i] != hex.EncodeToString(sum[:]) {
			t.Fatalf("line %d hash = %q, want %q", i, target.LineHashes[i], hex.EncodeToString(sum[:]))
		}
	}
}
