package ticket

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"

	"github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/base"
	tktapi "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/ticket-manager/ticket"
	dispatchent "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/ticket-manager/dispatch"
	tktent "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/ticket-manager/ticket"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/apiserver"
	jiracli "github.com/tidbcloud/aisupport/packages/sdk-go/infra/jira/client"
	"github.com/tidbcloud/aisupport/services/ticket-manager/internal/app"
	ticketsvcapi "github.com/tidbcloud/aisupport/services/ticket-manager/internal/services/ticket/api"
	"gorm.io/gorm"
)

func TestTicketsHandler_ListTicketsGetTicketAndListTimeline(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	ticketService := ticketsvcapi.NewMockService(ctrl)

	now := time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC)
	record := &tktent.Ticket{
		ID:                1001,
		JiraIssueKey:      "DEMO-20",
		JiraIssueID:       "jira-20",
		Title:             "Region unavailable",
		CurrentJiraStatus: "waiting for support",
		StatusEnteredAt:   now,
		CreatedAt:         now,
		UpdatedAt:         now,
	}
	event := &tktent.TimelineEvent{
		ID:         2001,
		TicketID:   1001,
		EventType:  "jira_status_changed",
		OccurredAt: now,
		ActorType:  tktent.ActorTypeSystem,
		ActorID:    "jira",
		Source:     tktent.SourceJira,
		Summary:    "Status changed",
	}

	ticketService.EXPECT().ListTickets(gomock.Any(), ticketsvcapi.ListTicketsParam{}).Return([]*tktent.Ticket{record}, nil)
	ticketService.EXPECT().GetTicketByIssueKey(gomock.Any(), "DEMO-20").Return(record, nil).Times(2)
	ticketService.EXPECT().ListTimelineEvents(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, param ticketsvcapi.ListTimelineEventsParam) ([]*tktent.TimelineEvent, error) {
		require.NotNil(t, param.TicketID)
		require.Equal(t, uint64(1001), *param.TicketID)
		return []*tktent.TimelineEvent{event}, nil
	})

	handler := newTestTicketsHandler(ticketService, jiracli.NewMockJiraClient(ctrl), now)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/ticketmanager/tickets", nil)
	rec := httptest.NewRecorder()
	serveTicketsHandler(t, handler, rec, req)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())

	var listResp tktapi.ListTicketsResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&listResp))
	require.NotNil(t, listResp.BaseResponse)
	require.Equal(t, http.StatusOK, listResp.ErrCode)
	require.Len(t, listResp.Tickets, 1)
	require.Equal(t, "DEMO-20", listResp.Tickets[0].JiraIssueKey)

	req = httptest.NewRequest(http.MethodGet, "/api/v1/ticketmanager/tickets/DEMO-20", nil)
	rec = httptest.NewRecorder()
	serveTicketsHandler(t, handler, rec, req)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())

	var getResp tktapi.GetTicketResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&getResp))
	require.NotNil(t, getResp.BaseResponse)
	require.Equal(t, http.StatusOK, getResp.ErrCode)
	require.NotNil(t, getResp.Ticket)
	require.Equal(t, uint64(1001), getResp.Ticket.ID)

	req = httptest.NewRequest(http.MethodGet, "/api/v1/ticketmanager/tickets/DEMO-20/timeline", nil)
	rec = httptest.NewRecorder()
	serveTicketsHandler(t, handler, rec, req)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())

	var timelineResp tktapi.GetTicketTimelineResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&timelineResp))
	require.NotNil(t, timelineResp.BaseResponse)
	require.Equal(t, http.StatusOK, timelineResp.ErrCode)
	require.Len(t, timelineResp.Events, 1)
	require.Equal(t, uint64(2001), timelineResp.Events[0].ID)
}

func TestTicketsHandler_GetTicketReturnsNotFound(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	ticketService := ticketsvcapi.NewMockService(ctrl)
	ticketService.EXPECT().GetTicketByIssueKey(gomock.Any(), "DEMO-404").Return(nil, gorm.ErrRecordNotFound)

	handler := newTestTicketsHandler(ticketService, jiracli.NewMockJiraClient(ctrl), time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC))

	req := httptest.NewRequest(http.MethodGet, "/api/v1/ticketmanager/tickets/DEMO-404", nil)
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusNotFound, rec.Code)
	require.Contains(t, rec.Body.String(), "record not found")
}

func TestTicketsHandler_CreateTicket(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	ticketService := ticketsvcapi.NewMockService(ctrl)
	ticketService.EXPECT().CreateTicket(gomock.Any(), ticketsvcapi.CreateTicketParam{SyncFromIssue: "DEMO-50"}).Return(nil)

	handler := newTestTicketsHandler(ticketService, jiracli.NewMockJiraClient(ctrl), time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC))

	req := httptest.NewRequest(http.MethodPost, "/api/v1/ticketmanager/tickets/DEMO-50", bytes.NewReader(mustMarshal(t, tktapi.CreateTicketRequest{})))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())

	var resp tktapi.CreateTicketResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	require.Equal(t, http.StatusOK, resp.ErrCode)
	require.Empty(t, resp.ErrMessage)
	require.NotNil(t, resp.DispatchTrigger)
	require.Equal(t, "ticket_ingest", resp.DispatchTrigger.Source)
}

func TestTicketsHandler_CreateTicketWithMockJiraIssue(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	ticketService := ticketsvcapi.NewMockService(ctrl)
	ticketService.EXPECT().CreateTicket(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, param ticketsvcapi.CreateTicketParam) error {
		require.Equal(t, "DEMO-50", param.SyncFromIssue)
		require.NotNil(t, param.JiraIssue)
		require.Equal(t, "DEMO-50", param.JiraIssue.Key)
		require.Equal(t, "jira-50", param.JiraIssue.ID)
		return nil
	})

	handler := newTestTicketsHandler(ticketService, jiracli.NewMockJiraClient(ctrl), time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC))

	body, err := json.Marshal(tktapi.CreateTicketRequest{
		JiraIssue: &tktent.JiraIssue{
			ID:        "jira-50",
			Summary:   "Mock ticket",
			Status:    "waiting for support",
			CreatedAt: time.Date(2026, 4, 18, 10, 0, 0, 0, time.UTC).Unix(),
			UpdatedAt: time.Date(2026, 4, 22, 10, 30, 0, 0, time.UTC).Unix(),
		},
	})
	require.NoError(t, err)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/ticketmanager/tickets/DEMO-50", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
}

func TestTicketsHandler_CreateTicketRejectsMismatchedMockIssueKey(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	handler := newTestTicketsHandler(ticketsvcapi.NewMockService(ctrl), jiracli.NewMockJiraClient(ctrl), time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC))

	body, err := json.Marshal(tktapi.CreateTicketRequest{
		JiraIssue: &tktent.JiraIssue{Key: "DEMO-OTHER"},
	})
	require.NoError(t, err)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/ticketmanager/tickets/DEMO-50", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusBadRequest, rec.Code)
	require.Contains(t, rec.Body.String(), "jiraIssue.key must match jiraIssueKey")
}

func TestTicketsHandler_CreateTicketReturnsConflictForDuplicate(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	ticketService := ticketsvcapi.NewMockService(ctrl)
	ticketService.EXPECT().CreateTicket(gomock.Any(), ticketsvcapi.CreateTicketParam{SyncFromIssue: "DEMO-50"}).Return(errors.New("ticket for issue DEMO-50 is existing"))

	handler := newTestTicketsHandler(ticketService, jiracli.NewMockJiraClient(ctrl), time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC))

	req := httptest.NewRequest(http.MethodPost, "/api/v1/ticketmanager/tickets/DEMO-50", bytes.NewReader(mustMarshal(t, tktapi.CreateTicketRequest{})))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusInternalServerError, rec.Code)
	require.Contains(t, rec.Body.String(), "ticket for issue DEMO-50 is existing")
}

func TestTicketsHandler_CreateTicketReturnsInternalServerError(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	ticketService := ticketsvcapi.NewMockService(ctrl)
	ticketService.EXPECT().CreateTicket(gomock.Any(), ticketsvcapi.CreateTicketParam{SyncFromIssue: "DEMO-50"}).Return(errors.New("jira failed"))

	handler := newTestTicketsHandler(ticketService, jiracli.NewMockJiraClient(ctrl), time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC))

	req := httptest.NewRequest(http.MethodPost, "/api/v1/ticketmanager/tickets/DEMO-50", bytes.NewReader(mustMarshal(t, tktapi.CreateTicketRequest{})))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusInternalServerError, rec.Code)
	require.Contains(t, rec.Body.String(), "jira failed")
}

func TestTicketsHandler_UpdateTicketJiraIssue(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	ticketService := ticketsvcapi.NewMockService(ctrl)
	ticketService.EXPECT().UpdateTicketJiraIssue(gomock.Any(), ticketsvcapi.UpdateTicketJiraIssueParam{SyncFromIssue: "DEMO-51"}).Return(nil)

	handler := newTestTicketsHandler(ticketService, jiracli.NewMockJiraClient(ctrl), time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC))

	req := httptest.NewRequest(http.MethodPut, "/api/v1/ticketmanager/tickets/DEMO-51/jira_issue", bytes.NewReader(mustMarshal(t, tktapi.UpdateTicketJiraIssueRequest{})))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())

	var resp base.BaseResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	require.Equal(t, http.StatusOK, resp.ErrCode)
	require.Empty(t, resp.ErrMessage)
}

func TestTicketsHandler_UpdateTicketJiraIssueWithMockJiraIssue(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	ticketService := ticketsvcapi.NewMockService(ctrl)
	ticketService.EXPECT().UpdateTicketJiraIssue(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, param ticketsvcapi.UpdateTicketJiraIssueParam) error {
		require.Equal(t, "DEMO-51", param.SyncFromIssue)
		require.NotNil(t, param.JiraIssue)
		require.Equal(t, "DEMO-51", param.JiraIssue.Key)
		require.Equal(t, "jira-51", param.JiraIssue.ID)
		return nil
	})

	handler := newTestTicketsHandler(ticketService, jiracli.NewMockJiraClient(ctrl), time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC))

	body, err := json.Marshal(tktapi.UpdateTicketJiraIssueRequest{
		JiraIssue: &tktent.JiraIssue{
			ID:        "jira-51",
			Key:       "DEMO-51",
			Summary:   "Updated mock ticket",
			Status:    "waiting for customer",
			CreatedAt: time.Date(2026, 4, 18, 10, 0, 0, 0, time.UTC).Unix(),
			UpdatedAt: time.Date(2026, 4, 22, 10, 30, 0, 0, time.UTC).Unix(),
		},
	})
	require.NoError(t, err)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/ticketmanager/tickets/DEMO-51/jira_issue", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
}

func TestTicketsHandler_UpdateTicketJiraIssueRejectsMismatchedMockIssueKey(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	handler := newTestTicketsHandler(ticketsvcapi.NewMockService(ctrl), jiracli.NewMockJiraClient(ctrl), time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC))

	body, err := json.Marshal(tktapi.UpdateTicketJiraIssueRequest{
		JiraIssue: &tktent.JiraIssue{Key: "DEMO-OTHER"},
	})
	require.NoError(t, err)

	req := httptest.NewRequest(http.MethodPut, "/api/v1/ticketmanager/tickets/DEMO-51/jira_issue", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusBadRequest, rec.Code)
	require.Contains(t, rec.Body.String(), "jiraIssue.key must match jiraIssueKey")
}

func TestTicketsHandler_UpdateTicketJiraIssueReturnsNotFound(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	ticketService := ticketsvcapi.NewMockService(ctrl)
	ticketService.EXPECT().UpdateTicketJiraIssue(gomock.Any(), ticketsvcapi.UpdateTicketJiraIssueParam{SyncFromIssue: "DEMO-404"}).Return(gorm.ErrRecordNotFound)

	handler := newTestTicketsHandler(ticketService, jiracli.NewMockJiraClient(ctrl), time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC))

	req := httptest.NewRequest(http.MethodPut, "/api/v1/ticketmanager/tickets/DEMO-404/jira_issue", bytes.NewReader(mustMarshal(t, tktapi.UpdateTicketJiraIssueRequest{})))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusNotFound, rec.Code)
	require.Contains(t, rec.Body.String(), "record not found")
}

func TestTicketsHandler_UpdateTicketJiraIssueReturnsInternalServerError(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	ticketService := ticketsvcapi.NewMockService(ctrl)
	ticketService.EXPECT().UpdateTicketJiraIssue(gomock.Any(), ticketsvcapi.UpdateTicketJiraIssueParam{SyncFromIssue: "DEMO-51"}).Return(errors.New("jira failed"))

	handler := newTestTicketsHandler(ticketService, jiracli.NewMockJiraClient(ctrl), time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC))

	req := httptest.NewRequest(http.MethodPut, "/api/v1/ticketmanager/tickets/DEMO-51/jira_issue", bytes.NewReader(mustMarshal(t, tktapi.UpdateTicketJiraIssueRequest{})))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusInternalServerError, rec.Code)
	require.Contains(t, rec.Body.String(), "jira failed")
}

func TestTicketsHandler_DoesNotRegisterLegacyAssignmentRoute(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	handler := newTestTicketsHandler(ticketsvcapi.NewMockService(ctrl), jiracli.NewMockJiraClient(ctrl), time.Now())
	req := httptest.NewRequest(http.MethodPut, "/api/v1/ticketmanager/tickets/DEMO-10/assignment", bytes.NewReader([]byte(`{"assignee":"alice"}`)))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusNotFound, rec.Code)
}

func TestTicketsHandler_DoesNotRegisterLegacyTriageRoute(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	handler := newTestTicketsHandler(
		ticketsvcapi.NewMockService(ctrl),
		jiracli.NewMockJiraClient(ctrl),
		time.Date(2026, 4, 30, 14, 0, 0, 0, time.UTC),
	)
	req := httptest.NewRequest(http.MethodPut, "/api/v1/ticketmanager/tickets/DEMO-30/triage", bytes.NewReader([]byte(`{}`)))
	rec := httptest.NewRecorder()

	serveTicketsHandler(t, handler, rec, req)

	require.Equal(t, http.StatusNotFound, rec.Code)
}

func serveTicketsHandler(t *testing.T, handler apiserver.Handler, rec *httptest.ResponseRecorder, req *http.Request) {
	t.Helper()

	gin.SetMode(gin.TestMode)
	router := gin.New()
	err := handler.Register(map[apiserver.RoutePath]gin.IRouter{
		app.APIRoutePath: router.Group(string(app.APIRoutePath)),
	})
	require.NoError(t, err)

	router.ServeHTTP(rec, req)
}

func newTestTicketsHandler(
	ticketService ticketsvcapi.Service,
	_ jiracli.JiraClient,
	_ time.Time,
) *TicketsHandler {
	return &TicketsHandler{
		ticketService: ticketService,
		ingestService: &testIngestService{ticket: ticketService},
	}
}

type testIngestService struct {
	ticket ticketsvcapi.Service
}

func (s *testIngestService) CreateTicket(ctx context.Context, param ticketsvcapi.CreateTicketParam) (*dispatchent.DispatchTrigger, error) {
	if err := s.ticket.CreateTicket(ctx, param); err != nil {
		return nil, err
	}
	return &dispatchent.DispatchTrigger{Source: "ticket_ingest"}, nil
}

func mustMarshal(t *testing.T, value any) []byte {
	t.Helper()

	data, err := json.Marshal(value)
	require.NoError(t, err)
	return data
}
