package wiki

import (
	"encoding/base64"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"github.com/gitlink-org/gitlink-cli/internal/client"
	"github.com/gitlink-org/gitlink-cli/internal/output"
	"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)

func resetProjectIDCache() {
	projectIDCache = sync.Map{}
}

func newMockServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
	t.Helper()
	return httptest.NewServer(handler)
}

func writeJSON(t *testing.T, w http.ResponseWriter, v interface{}) {
	t.Helper()
	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(v); err != nil {
		t.Fatalf("writeJSON: %v", err)
	}
}

// ---- unwrapGatewayResponse tests ----

func TestUnwrapGatewayResponse_Success(t *testing.T) {
	env := output.SuccessEnvelope(map[string]interface{}{
		"code": float64(200),
		"data": map[string]interface{}{"id": float64(1)},
	}, nil)

	result, err := unwrapGatewayResponse(env)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	data, ok := result.Data.(map[string]interface{})
	if !ok {
		t.Fatal("expected map data")
	}
	if data["id"] != float64(1) {
		t.Fatalf("got %v, want 1", data["id"])
	}
}

func TestUnwrapGatewayResponse_Code201(t *testing.T) {
	env := output.SuccessEnvelope(map[string]interface{}{
		"code": float64(201),
		"data": "ok",
	}, nil)

	result, err := unwrapGatewayResponse(env)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.Data != "ok" {
		t.Fatalf("got %v, want ok", result.Data)
	}
}

func TestUnwrapGatewayResponse_BusinessError(t *testing.T) {
	env := output.SuccessEnvelope(map[string]interface{}{
		"code": float64(500),
		"msg":  "内部错误",
	}, nil)

	_, err := unwrapGatewayResponse(env)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if err.Error() != "[500] 内部错误" {
		t.Fatalf("got %q, want %q", err.Error(), "[500] 内部错误")
	}
}

func TestUnwrapGatewayResponse_NoDataField(t *testing.T) {
	env := output.SuccessEnvelope(map[string]interface{}{
		"code": float64(200),
	}, nil)

	result, err := unwrapGatewayResponse(env)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// Should return original envelope since no "data" field to extract
	if result != env {
		t.Fatal("expected original envelope when no data field")
	}
}

func TestUnwrapGatewayResponse_NonMapData(t *testing.T) {
	env := output.SuccessEnvelope("plain text", nil)

	result, err := unwrapGatewayResponse(env)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if result.Data != "plain text" {
		t.Fatalf("got %v, want plain text", result.Data)
	}
}

// ---- resolveProjectID tests ----

func TestResolveProjectID_Success(t *testing.T) {
	resetProjectIDCache()
	server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/owner1/repo1/detail.json" {
			writeJSON(t, w, map[string]interface{}{"project_id": float64(123)})
			return
		}
		t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
	})
	defer server.Close()

	ctx := &common.RuntimeContext{
		Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
		Owner:  "owner1",
		Repo:   "repo1",
	}
	pid, err := resolveProjectID(ctx)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if pid != "123" {
		t.Fatalf("got %q, want %q", pid, "123")
	}
}

func TestResolveProjectID_Float64(t *testing.T) {
	resetProjectIDCache()
	server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/owner2/repo2/detail.json" {
			writeJSON(t, w, map[string]interface{}{"project_id": float64(456)})
			return
		}
		t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
	})
	defer server.Close()

	ctx := &common.RuntimeContext{
		Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
		Owner:  "owner2",
		Repo:   "repo2",
	}
	pid, err := resolveProjectID(ctx)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if pid != "456" {
		t.Fatalf("got %q, want %q", pid, "456")
	}
}

func TestResolveProjectID_CacheHit(t *testing.T) {
	resetProjectIDCache()
	callCount := 0
	server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		callCount++
		if r.URL.Path == "/owner3/repo3/detail.json" {
			writeJSON(t, w, map[string]interface{}{"project_id": float64(789)})
			return
		}
		t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
	})
	defer server.Close()

	ctx := &common.RuntimeContext{
		Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
		Owner:  "owner3",
		Repo:   "repo3",
	}

	pid1, err := resolveProjectID(ctx)
	if err != nil {
		t.Fatalf("first call: %v", err)
	}
	if pid1 != "789" {
		t.Fatalf("first call: got %q, want %q", pid1, "789")
	}

	pid2, err := resolveProjectID(ctx)
	if err != nil {
		t.Fatalf("second call: %v", err)
	}
	if pid2 != "789" {
		t.Fatalf("second call: got %q, want %q", pid2, "789")
	}

	if callCount != 1 {
		t.Fatalf("API called %d times, want 1 (cache miss)", callCount)
	}
}

func TestResolveProjectID_MissingProjectID(t *testing.T) {
	resetProjectIDCache()
	server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		writeJSON(t, w, map[string]interface{}{"name": "no-project-id"})
	})
	defer server.Close()

	ctx := &common.RuntimeContext{
		Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
		Owner:  "owner4",
		Repo:   "repo4",
	}
	_, err := resolveProjectID(ctx)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

func TestResolveProjectID_APIError(t *testing.T) {
	resetProjectIDCache()
	server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
	})
	defer server.Close()

	ctx := &common.RuntimeContext{
		Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
		Owner:  "owner5",
		Repo:   "repo5",
	}
	_, err := resolveProjectID(ctx)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

// ---- callWikiAPI HTTP request path tests ----

func TestCallWikiAPI_Success(t *testing.T) {
	resetProjectIDCache()
	var receivedPath, receivedMethod string
	var receivedBody []byte
	server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		receivedPath = r.URL.Path
		receivedMethod = r.Method
		if r.Body != nil {
			buf := make([]byte, 1024)
			n, _ := r.Body.Read(buf)
			receivedBody = buf[:n]
		}
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"code":200,"msg":"ok","data":{"id":42}}`))
	})
	defer server.Close()

	ctx := &common.RuntimeContext{
		Client:            &client.Client{HTTP: server.Client(), BaseURL: server.URL},
		GatewayBaseURL:    server.URL,
		GatewayHTTPClient: server.Client(),
	}

	env, err := callWikiAPI(ctx, "POST", "/wiki/open/test", map[string]string{"foo": "bar"})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if receivedMethod != "POST" {
		t.Errorf("method = %q, want POST", receivedMethod)
	}
	if receivedPath != "/wiki/open/test" {
		t.Errorf("path = %q, want /wiki/open/test", receivedPath)
	}
	if !strings.Contains(string(receivedBody), `"foo"`) {
		t.Errorf("body should contain foo: %s", string(receivedBody))
	}
	data, ok := env.Data.(map[string]interface{})
	if !ok {
		t.Fatalf("expected map data, got %T", env.Data)
	}
	if data["id"] != float64(42) {
		t.Errorf("data[id] = %v, want 42", data["id"])
	}
}

func TestCallWikiAPI_BusinessError(t *testing.T) {
	resetProjectIDCache()
	server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"code":400,"msg":"bad request","data":null}`))
	})
	defer server.Close()

	ctx := &common.RuntimeContext{
		Client:            &client.Client{HTTP: server.Client(), BaseURL: server.URL},
		GatewayBaseURL:    server.URL,
		GatewayHTTPClient: server.Client(),
	}
	_, err := callWikiAPI(ctx, "GET", "/test", nil)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
	if !strings.Contains(err.Error(), "400") || !strings.Contains(err.Error(), "bad request") {
		t.Errorf("err = %q, want to contain 400 and bad request", err.Error())
	}
}

func TestCallWikiAPI_GatewayHTTPError(t *testing.T) {
	resetProjectIDCache()
	server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusBadGateway)
		w.Write([]byte(`upstream error`))
	})
	defer server.Close()

	ctx := &common.RuntimeContext{
		Client:            &client.Client{HTTP: server.Client(), BaseURL: server.URL},
		GatewayBaseURL:    server.URL,
		GatewayHTTPClient: server.Client(),
	}
	_, err := callWikiAPI(ctx, "GET", "/test", nil)
	if err == nil {
		t.Fatal("expected error on 502")
	}
}

func TestCallWikiAPI_SkipsJSONSuffix(t *testing.T) {
	// Verifies the SkipJSONSuffix path is correctly taken for gateway:
	// the URL should NOT have a .json appended.
	resetProjectIDCache()
	var receivedPath string
	server := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		receivedPath = r.URL.Path
		w.Write([]byte(`{"code":200,"data":{}}`))
	})
	defer server.Close()

	ctx := &common.RuntimeContext{
		Client:            &client.Client{HTTP: server.Client(), BaseURL: server.URL},
		GatewayBaseURL:    server.URL,
		GatewayHTTPClient: server.Client(),
	}
	_, err := callWikiAPI(ctx, "GET", "/wiki/open/list", nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if receivedPath != "/wiki/open/list" {
		t.Errorf("path = %q, want /wiki/open/list (no .json suffix)", receivedPath)
	}
	if strings.HasSuffix(receivedPath, ".json") {
		t.Errorf("path %q should NOT have .json suffix (gateway expects no suffix)", receivedPath)
	}
}

func TestCallWikiAPI_ConnectionRefused(t *testing.T) {
	resetProjectIDCache()
	// Use an unbound port to simulate connection failure
	ctx := &common.RuntimeContext{
		Client:            &client.Client{HTTP: &http.Client{}, BaseURL: "http://127.0.0.1:1"},
		GatewayBaseURL:    "http://127.0.0.1:1",
		GatewayHTTPClient: &http.Client{},
	}
	_, err := callWikiAPI(ctx, "GET", "/test", nil)
	if err == nil {
		t.Fatal("expected connection error")
	}
}

// ---- runLint / check rules tests ----

func TestIsCheckEnabled_Empty(t *testing.T) {
	if !isCheckEnabled("", "any") {
		t.Error("empty filter should enable all checks")
	}
	if !isCheckEnabled("empty,headings", "empty") {
		t.Error("should enable 'empty' in filter list")
	}
	if isCheckEnabled("headings", "empty") {
		t.Error("should not enable 'empty' when not in filter list")
	}
	if !isCheckEnabled("  empty  , headings ", "empty") {
		t.Error("should trim whitespace")
	}
}

func TestCheckEmpty(t *testing.T) {
	issues := checkEmpty("p1", "")
	if len(issues) != 1 || issues[0].Level != "error" || issues[0].Check != "empty" {
		t.Errorf("expected 1 error-level 'empty' issue, got %+v", issues)
	}
	if issues := checkEmpty("p1", "some content"); issues != nil {
		t.Errorf("non-empty content should not produce issues, got %+v", issues)
	}
	if issues := checkEmpty("p1", "   \n\t  "); len(issues) != 1 {
		t.Errorf("whitespace-only content should be empty, got %+v", issues)
	}
}

func TestCheckHeading(t *testing.T) {
	// missing H1
	if issues := checkHeading("p1", "Some text without heading"); len(issues) != 1 {
		t.Errorf("expected 1 missing-heading issue, got %+v", issues)
	}
	// has H1
	if issues := checkHeading("p1", "# Title\nbody"); issues != nil {
		t.Errorf("H1 should not produce issues, got %+v", issues)
	}
	// empty content skipped
	if issues := checkHeading("p1", ""); issues != nil {
		t.Errorf("empty content should be skipped, got %+v", issues)
	}
	// whitespace prefix
	if issues := checkHeading("p1", "  \n# Real Title"); issues != nil {
		t.Errorf("H1 after whitespace should not produce issues, got %+v", issues)
	}
}

func TestCheckShort(t *testing.T) {
	if issues := checkShort("p1", ""); issues != nil {
		t.Errorf("empty content should be skipped, got %+v", issues)
	}
	if issues := checkShort("p1", "short"); len(issues) != 1 {
		t.Errorf("expected 1 short issue, got %+v", issues)
	}
	long := strings.Repeat("a", 100)
	if issues := checkShort("p1", long); issues != nil {
		t.Errorf("long content should not produce issues, got %+v", issues)
	}
	// exactly 49 chars triggers
	if issues := checkShort("p1", strings.Repeat("a", 49)); len(issues) != 1 {
		t.Errorf("49-char content should be 'short', got %+v", issues)
	}
}

func TestCheckDeadLinks(t *testing.T) {
	known := map[string]bool{"Home": true, "Guide": true}

	// All known: no issues
	if issues := checkDeadLinks("p1", "[Home](Home) and [Guide](Guide)", known); issues != nil {
		t.Errorf("all-known should not produce issues, got %+v", issues)
	}
	// Unknown link
	issues := checkDeadLinks("p1", "[Unknown](Unknown)", known)
	if len(issues) != 1 || issues[0].Check != "links" {
		t.Errorf("expected 1 dead link issue, got %+v", issues)
	}
	// External links skipped
	if issues := checkDeadLinks("p1", "[ext](https://example.com)", known); issues != nil {
		t.Errorf("external links should be skipped, got %+v", issues)
	}
	// Anchor links skipped
	if issues := checkDeadLinks("p1", "[anchor](#section)", known); issues != nil {
		t.Errorf("anchor links should be skipped, got %+v", issues)
	}
	// Mixed
	issues = checkDeadLinks("p1", "[Home](Home) and [Bad](BadPage)", known)
	if len(issues) != 1 {
		t.Errorf("expected 1 dead link in mixed, got %+v", issues)
	}
	// Empty content
	if issues := checkDeadLinks("p1", "", known); issues != nil {
		t.Errorf("empty content should not produce issues, got %+v", issues)
	}
}

func TestCheckImages(t *testing.T) {
	// Mock image server
	imgServer := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "HEAD" {
			w.WriteHeader(http.StatusOK)
			return
		}
		w.WriteHeader(http.StatusOK)
	})
	defer imgServer.Close()

	brokenServer := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNotFound)
	})
	defer brokenServer.Close()

	httpClient := imgServer.Client()
	// Valid image (200)
	if issues := checkImages("p1", "![ok]("+imgServer.URL+"/img.png)", httpClient); issues != nil {
		t.Errorf("200 image should not produce issues, got %+v", issues)
	}
	// Broken image (404)
	issues := checkImages("p1", "![bad]("+brokenServer.URL+"/missing.png)", httpClient)
	if len(issues) != 1 {
		t.Errorf("expected 1 broken image issue, got %+v", issues)
	}
	// No images
	if issues := checkImages("p1", "no images here", httpClient); issues != nil {
		t.Errorf("no images should not produce issues, got %+v", issues)
	}
	// Malformed HTTP URL (regex matches https?:// but http.NewRequest fails to parse)
	if issues := checkImages("p1", "![bad](http://[)", httpClient); len(issues) != 1 {
		t.Errorf("expected 1 invalid-URL issue, got %+v", issues)
	}
}

func TestRunLint_Integration(t *testing.T) {
	resetProjectIDCache()

	// Mock main API (project detail) and gateway (wiki list + get)
	mainServer := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		if strings.HasSuffix(r.URL.Path, "/detail.json") {
			writeJSON(t, w, map[string]interface{}{"project_id": float64(999)})
			return
		}
		t.Errorf("unexpected main API call: %s %s", r.Method, r.URL.Path)
	})
	defer mainServer.Close()

	// Page content (base64 encoded)
	goodContent := base64.StdEncoding.EncodeToString([]byte("# Good Page\n\n" + strings.Repeat("This is a well-formed page with enough content to pass the short check. ", 3)))

	wikiServer := newMockServer(t, func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/wiki/open/wikiPages":
			writeJSON(t, w, map[string]interface{}{
				"code": 200,
				"data": []map[string]interface{}{
					{"title": "Good", "sub_url": "Good"},
					{"title": "Empty", "sub_url": "Empty"},
					{"title": "_Sidebar", "sub_url": "_Sidebar"}, // system page - skipped
				},
			})
		case "/wiki/open/getWiki":
			pageName := r.URL.Query().Get("pageName")
			var content string
			if pageName == "Empty" {
				content = "" // empty page
			} else {
				content = goodContent
			}
			writeJSON(t, w, map[string]interface{}{
				"code": 200,
				"data": map[string]interface{}{"content_base64": content},
			})
		default:
			t.Errorf("unexpected wiki path: %s", r.URL.Path)
		}
	})
	defer wikiServer.Close()

	ctx := &common.RuntimeContext{
		Client:            &client.Client{HTTP: mainServer.Client(), BaseURL: mainServer.URL},
		Owner:             "owner1",
		Repo:              "repo1",
		Format:            "json",
		GatewayBaseURL:    wikiServer.URL,
		GatewayHTTPClient: wikiServer.Client(),
	}

	if err := runLint(ctx); err != nil {
		t.Fatalf("runLint: %v", err)
	}
	// _Sidebar is skipped, so TotalPages=2 (Good, Empty)
	// Empty page produces 1 "empty" error
	// We can't directly inspect the output envelope, but if no error, the function ran end-to-end
}

// ---- resolveContent tests ----

func TestResolveContent_FromArg(t *testing.T) {
	ctx := &common.RuntimeContext{
		Args: map[string]string{"content": "inline content"},
	}
	got, err := resolveContent(ctx)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != "inline content" {
		t.Errorf("got %q, want %q", got, "inline content")
	}
}

func TestResolveContent_FromFile(t *testing.T) {
	tmpDir := t.TempDir()
	path := filepath.Join(tmpDir, "wiki.md")
	want := "# Title\n\nBody content from file"
	if err := os.WriteFile(path, []byte(want), 0600); err != nil {
		t.Fatalf("setup: %v", err)
	}
	ctx := &common.RuntimeContext{
		Args: map[string]string{"file": path},
	}
	got, err := resolveContent(ctx)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != want {
		t.Errorf("got %q, want %q", got, want)
	}
}

func TestResolveContent_ArgTakesPrecedence(t *testing.T) {
	// When both --content and --file are set, --content wins
	tmpDir := t.TempDir()
	path := filepath.Join(tmpDir, "wiki.md")
	if err := os.WriteFile(path, []byte("from file"), 0600); err != nil {
		t.Fatalf("setup: %v", err)
	}
	ctx := &common.RuntimeContext{
		Args: map[string]string{
			"content": "from arg",
			"file":    path,
		},
	}
	got, err := resolveContent(ctx)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != "from arg" {
		t.Errorf("arg should take precedence; got %q", got)
	}
}

func TestResolveContent_Missing(t *testing.T) {
	ctx := &common.RuntimeContext{
		Args: map[string]string{},
	}
	_, err := resolveContent(ctx)
	if err == nil {
		t.Fatal("expected error when neither --content nor --file is provided")
	}
	if !strings.Contains(err.Error(), "required") {
		t.Errorf("err = %q, want to mention 'required'", err.Error())
	}
}

func TestResolveContent_FileNotFound(t *testing.T) {
	ctx := &common.RuntimeContext{
		Args: map[string]string{"file": "/nonexistent/path/to/wiki.md"},
	}
	_, err := resolveContent(ctx)
	if err == nil {
		t.Fatal("expected error for nonexistent file")
	}
}
