package handler

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
	"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)

// acceptSetup writes a tree with a conforming transmittal folder under
// archive/Acme/incoming/ and an admin grant for alice. Returns the cfg,
// a do() helper, and the root path.
func acceptSetup(t *testing.T) (config.Config, func(target, email string, body []byte) *httptest.ResponseRecorder, string) {
	t.Helper()
	root := t.TempDir()
	mustWriteHelper(t, filepath.Join(root, ".zddc"),
		"admins:\n  - alice@example.com\n"+
			"roles:\n  document_controller:\n    members: [alice@example.com]\n")
	for _, d := range []string{"Project-1/archive/Acme/incoming/2026-05-15_Acme-0042 (RFI) - Foundation"} {
		if err := os.MkdirAll(filepath.Join(root, d), 0o755); err != nil {
			t.Fatalf("mkdir %s: %v", d, err)
		}
	}
	// Seed two conforming files inside the transmittal folder.
	transmittalDir := filepath.Join(root, "Project-1/archive/Acme/incoming/2026-05-15_Acme-0042 (RFI) - Foundation")
	mustWriteHelper(t, filepath.Join(transmittalDir, "Acme-0042_A (RFI) - Foundation.pdf"), "%PDF-")
	mustWriteHelper(t, filepath.Join(transmittalDir, "Acme-0042_A (RFI) - Cover Letter.pdf"), "%PDF-")
	zddc.InvalidateCache(root)

	cfg := config.Config{
		Root:          root,
		EmailHeader:   "X-Auth-Request-Email",
		MaxWriteBytes: 64 * 1024,
	}
	do := func(target, email string, body []byte) *httptest.ResponseRecorder {
		// target may contain spaces and parens (real transmittal folder
		// names do); construct the URL from a url.URL so the request line
		// gets properly escaped and r.URL.Path comes back decoded for the
		// handler's pattern match.
		u := &url.URL{Path: target}
		req := httptest.NewRequest(http.MethodPost, u.RequestURI(), bytes.NewReader(body))
		req.Header.Set(headerOp, opAcceptTransmittal)
		req.Header.Set("Content-Type", "application/yaml")
		ctx := context.WithValue(req.Context(), EmailKey, email)
		ctx = context.WithValue(ctx, ElevatedKey, true)
		req = req.WithContext(ctx)
		rec := httptest.NewRecorder()
		ServeFileAPI(cfg, rec, req)
		return rec
	}
	return cfg, do, root
}

// TestAccept_FreshAcceptance — a conforming transmittal folder moves
// from incoming/ to received/, renamed to tracking-only.
func TestAccept_FreshAcceptance(t *testing.T) {
	_, do, root := acceptSetup(t)
	target := "/Project-1/archive/Acme/incoming/2026-05-15_Acme-0042 (RFI) - Foundation/"
	rec := do(target, "alice@example.com", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var resp acceptResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode response: %v; body=%s", err, rec.Body.String())
	}
	if resp.Tracking != "Acme-0042" {
		t.Errorf("Tracking=%q, want Acme-0042", resp.Tracking)
	}
	if resp.MovedFiles != 2 {
		t.Errorf("MovedFiles=%d, want 2", resp.MovedFiles)
	}
	if resp.Merged {
		t.Errorf("Merged=true, want false on fresh acceptance")
	}
	// Folder should be at received/Acme-0042/, not the transmittal name.
	if _, err := os.Stat(filepath.Join(root, "Project-1/archive/Acme/received/Acme-0042/Acme-0042_A (RFI) - Foundation.pdf")); err != nil {
		t.Errorf("primary file not moved into received/: %v", err)
	}
	// Source should no longer exist.
	if _, err := os.Stat(filepath.Join(root, "Project-1/archive/Acme/incoming/2026-05-15_Acme-0042 (RFI) - Foundation")); !os.IsNotExist(err) {
		t.Errorf("source folder still present after rename")
	}
}

// TestAccept_NonConformingFilename — a file inside the transmittal
// folder that doesn't parse rejects the whole accept and leaves the
// source untouched.
func TestAccept_NonConformingFilename(t *testing.T) {
	_, do, root := acceptSetup(t)
	// Drop a bad file alongside the good ones.
	mustWriteHelper(t, filepath.Join(root, "Project-1/archive/Acme/incoming/2026-05-15_Acme-0042 (RFI) - Foundation/random-notes.txt"), "oops")
	rec := do("/Project-1/archive/Acme/incoming/2026-05-15_Acme-0042 (RFI) - Foundation/", "alice@example.com", nil)
	if rec.Code != http.StatusConflict {
		t.Fatalf("status=%d, want 409; body=%s", rec.Code, rec.Body.String())
	}
	if !strings.Contains(rec.Body.String(), "random-notes.txt") {
		t.Errorf("error body should name the violating file; got %s", rec.Body.String())
	}
	// Source untouched.
	if _, err := os.Stat(filepath.Join(root, "Project-1/archive/Acme/incoming/2026-05-15_Acme-0042 (RFI) - Foundation")); err != nil {
		t.Errorf("source folder removed despite rejection: %v", err)
	}
}

// TestAccept_NonConformingFolderName — a transmittal folder whose
// name doesn't parse rejects with 400 (the URL pattern matches the
// outer shape but the folder grammar fails).
func TestAccept_NonConformingFolderName(t *testing.T) {
	_, do, root := acceptSetup(t)
	badDir := filepath.Join(root, "Project-1/archive/Acme/incoming/bad-folder-name")
	if err := os.MkdirAll(badDir, 0o755); err != nil {
		t.Fatal(err)
	}
	rec := do("/Project-1/archive/Acme/incoming/bad-folder-name/", "alice@example.com", nil)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status=%d, want 400; body=%s", rec.Code, rec.Body.String())
	}
}

// TestAccept_PlanReviewChain — setup_plan_review: true chains into
// Plan Review and reports both results in the response.
func TestAccept_PlanReviewChain(t *testing.T) {
	_, do, root := acceptSetup(t)
	body := []byte(strings.Join([]string{
		"setup_plan_review: true",
		"review_lead: bob@vendor.com",
		"approver: carol@example.com",
		"plan_review_complete_date: 2026-05-30",
		"plan_response_date: 2026-06-15",
		"",
	}, "\n"))
	rec := do("/Project-1/archive/Acme/incoming/2026-05-15_Acme-0042 (RFI) - Foundation/", "alice@example.com", body)
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var resp acceptResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if resp.PlanReview == nil {
		t.Fatalf("PlanReview chain absent in response: %+v", resp)
	}
	if !resp.PlanReview.Reviewing.Created || !resp.PlanReview.Staging.Created {
		t.Errorf("chained Plan Review did not converge: %+v", resp.PlanReview)
	}
	// received/.zddc must exist (Plan Review writes it).
	if _, err := os.Stat(filepath.Join(root, "Project-1/archive/Acme/received/Acme-0042/.zddc")); err != nil {
		t.Errorf("received .zddc not written by chained Plan Review: %v", err)
	}
}

// TestAccept_Merge — a second acceptance of the same tracking with
// distinct filenames merges into the existing received/<tracking>/
// folder. Re-using a filename is rejected by WORM.
func TestAccept_Merge(t *testing.T) {
	_, do, root := acceptSetup(t)
	rec := do("/Project-1/archive/Acme/incoming/2026-05-15_Acme-0042 (RFI) - Foundation/", "alice@example.com", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("first accept status=%d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	// Build a second transmittal folder with the same tracking but a
	// distinct rev so the filenames don't collide.
	secondDir := filepath.Join(root, "Project-1/archive/Acme/incoming/2026-06-01_Acme-0042 (RFI) - Followup")
	if err := os.MkdirAll(secondDir, 0o755); err != nil {
		t.Fatal(err)
	}
	mustWriteHelper(t, filepath.Join(secondDir, "Acme-0042_B (RFI) - Foundation.pdf"), "%PDF-")
	rec = do("/Project-1/archive/Acme/incoming/2026-06-01_Acme-0042 (RFI) - Followup/", "alice@example.com", nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("second accept status=%d, want 200; body=%s", rec.Code, rec.Body.String())
	}
	var resp acceptResponse
	_ = json.Unmarshal(rec.Body.Bytes(), &resp)
	if !resp.Merged {
		t.Errorf("Merged=false on re-acceptance of same tracking; want true")
	}
	// Both revs should now live in received/Acme-0042/.
	for _, name := range []string{"Acme-0042_A (RFI) - Foundation.pdf", "Acme-0042_B (RFI) - Foundation.pdf"} {
		if _, err := os.Stat(filepath.Join(root, "Project-1/archive/Acme/received/Acme-0042", name)); err != nil {
			t.Errorf("expected %s in merged received/: %v", name, err)
		}
	}
}
