package router_test

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strconv"
	"testing"

	"git.godopu.com/lab/landing_page/backend/internal/db"
	"git.godopu.com/lab/landing_page/backend/internal/models"
	"git.godopu.com/lab/landing_page/backend/internal/router"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	_ "modernc.org/sqlite"
)

const testAdminToken = "test-secret-token"

func setupTestDB(t *testing.T) *sql.DB {
	tmpDB := filepath.Join(t.TempDir(), "test.db")
	database, err := db.Connect(tmpDB)
	require.NoError(t, err)

	err = db.RunMigrations(database)
	require.NoError(t, err)

	seedDir := filepath.Join("..", "..", "seed", "data")
	seedDatabase(t, database, seedDir)

	return database
}

func seedDatabase(t *testing.T, database *sql.DB, seedDir string) {
	tx, err := database.BeginTx(context.Background(), nil)
	require.NoError(t, err)
	defer tx.Rollback()

	// 1. Projects
	pData, err := os.ReadFile(filepath.Join(seedDir, "research_projects.json"))
	require.NoError(t, err)
	var projects []struct {
		Slug           string   `json:"slug"`
		Title          string   `json:"title"`
		Abstract       string   `json:"abstract"`
		Keywords       []string `json:"keywords"`
		StandardsTrack *string  `json:"standardsTrack"`
		StandardsOrg   *string  `json:"standardsOrg"`
		Period         *string  `json:"period"`
		Funder         *string  `json:"funder"`
	}
	require.NoError(t, json.Unmarshal(pData, &projects))
	for _, p := range projects {
		kwJSON, _ := json.Marshal(p.Keywords)
		_, err := tx.Exec(`
			INSERT INTO research_projects (slug, title, abstract, keywords, organization, standards_org, period, funder)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?)
		`, p.Slug, p.Title, p.Abstract, string(kwJSON), p.StandardsTrack, p.StandardsOrg, p.Period, p.Funder)
		require.NoError(t, err)
	}

	// 2. Areas
	aData, err := os.ReadFile(filepath.Join(seedDir, "research_areas.json"))
	require.NoError(t, err)
	var areas []struct {
		En string  `json:"en"`
		Kr *string `json:"kr"`
	}
	require.NoError(t, json.Unmarshal(aData, &areas))
	for idx, a := range areas {
		_, err := tx.Exec(`
			INSERT INTO research_areas (name_en, name_kr, display_order)
			VALUES (?, ?, ?)
		`, a.En, a.Kr, idx+1)
		require.NoError(t, err)
	}

	// 3. Members
	mData, err := os.ReadFile(filepath.Join(seedDir, "members.json"))
	require.NoError(t, err)
	var members []struct {
		Ko          string  `json:"ko"`
		En          string  `json:"en"`
		Degree      string  `json:"degree"`
		Affiliation *string `json:"affiliation"`
		Email       *string `json:"email"`
		IsAdvisor   bool    `json:"is_advisor"`
	}
	require.NoError(t, json.Unmarshal(mData, &members))
	for idx, m := range members {
		_, err := tx.Exec(`
			INSERT INTO members (name_ko, name_en, degree, affiliation, email, is_advisor, display_order)
			VALUES (?, ?, ?, ?, ?, ?, ?)
		`, m.Ko, m.En, m.Degree, m.Affiliation, m.Email, m.IsAdvisor, idx+1)
		require.NoError(t, err)
	}

	// 4. Alumni
	alumniData, err := os.ReadFile(filepath.Join(seedDir, "alumni.json"))
	require.NoError(t, err)
	var alumni []struct {
		Ko              string  `json:"ko"`
		En              string  `json:"en"`
		Degree          string  `json:"degree"`
		GraduatedAt     string  `json:"graduatedAt"`
		Major           *string `json:"major"`
		CurrentPosition *string `json:"currentPosition"`
	}
	require.NoError(t, json.Unmarshal(alumniData, &alumni))
	for _, a := range alumni {
		_, err := tx.Exec(`
			INSERT INTO alumni (name_ko, name_en, degree, graduated_at, major, current_position)
			VALUES (?, ?, ?, ?, ?, ?)
		`, a.Ko, a.En, a.Degree, a.GraduatedAt, a.Major, a.CurrentPosition)
		require.NoError(t, err)
	}

	// 5. Publications
	pubData, err := os.ReadFile(filepath.Join(seedDir, "publications.json"))
	require.NoError(t, err)
	var publications []struct {
		Category    string  `json:"category"`
		Title       string  `json:"title"`
		Authors     *string `json:"authors"`
		Venue       string  `json:"venue"`
		Volume      *string `json:"volume"`
		PublishedAt string  `json:"publishedAt"`
		KCI         bool    `json:"kci"`
		DOI         *string `json:"doi"`
		IsHighlight bool    `json:"is_highlight"`
	}
	require.NoError(t, json.Unmarshal(pubData, &publications))
	for _, p := range publications {
		_, err := tx.Exec(`
			INSERT INTO publications (category, title, authors, venue, volume, published_at, kci, doi, is_highlight)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
		`, p.Category, p.Title, p.Authors, p.Venue, p.Volume, p.PublishedAt, p.KCI, p.DOI, p.IsHighlight)
		require.NoError(t, err)
	}

	// 6. Patents
	patData, err := os.ReadFile(filepath.Join(seedDir, "patents.json"))
	require.NoError(t, err)
	var patents []struct {
		Title          string  `json:"title"`
		Inventors      string  `json:"inventors"`
		ApplicationNo  string  `json:"applicationNo"`
		ApplicationAt  string  `json:"applicationAt"`
		RegistrationNo *string `json:"registrationNo"`
		RegistrationAt *string `json:"registrationAt"`
		Country        string  `json:"country"`
		PublishedAt    string  `json:"publishedAt"`
	}
	require.NoError(t, json.Unmarshal(patData, &patents))
	for _, p := range patents {
		_, err := tx.Exec(`
			INSERT INTO patents (title, inventors, application_no, application_at, registration_no, registration_at, country, published_at)
			VALUES (?, ?, ?, ?, ?, ?, ?, ?)
		`, p.Title, p.Inventors, p.ApplicationNo, p.ApplicationAt, p.RegistrationNo, p.RegistrationAt, p.Country, p.PublishedAt)
		require.NoError(t, err)
	}

	// 7. Lectures
	lecData, err := os.ReadFile(filepath.Join(seedDir, "lectures.json"))
	require.NoError(t, err)
	var semesters []struct {
		Term    string `json:"term"`
		Year    int    `json:"year"`
		Courses []struct {
			Code string  `json:"code"`
			En   string  `json:"en"`
			Note *string `json:"note"`
		} `json:"courses"`
	}
	require.NoError(t, json.Unmarshal(lecData, &semesters))
	for _, s := range semesters {
		res, err := tx.Exec(`INSERT INTO semesters (year, term) VALUES (?, ?)`, s.Year, s.Term)
		require.NoError(t, err)
		semID, _ := res.LastInsertId()
		for cIdx, c := range s.Courses {
			_, err := tx.Exec(`
				INSERT INTO courses (semester_id, code, name_en, name_kr, note, display_order)
				VALUES (?, ?, ?, ?, ?, ?)
			`, semID, c.Code, c.En, nil, c.Note, cIdx+1)
			require.NoError(t, err)
		}
	}

	// 8. Standards
	stdData, err := os.ReadFile(filepath.Join(seedDir, "standards.json"))
	require.NoError(t, err)
	var standards []struct {
		Org      string  `json:"org"`
		Full     string  `json:"full"`
		Scope    string  `json:"scope"`
		Period   string  `json:"period"`
		Role     *string `json:"role"`
		Projects []struct {
			WG        string `json:"wg"`
			Name      string `json:"name"`
			Documents []struct {
				Title       string  `json:"title"`
				Ref         string  `json:"ref"`
				Status      string  `json:"status"`
				PublishedAt *string `json:"publishedAt"`
			} `json:"documents"`
		} `json:"projects"`
	}
	require.NoError(t, json.Unmarshal(stdData, &standards))
	for bIdx, b := range standards {
		res, err := tx.Exec(`
			INSERT INTO standards_bodies (org, full_name, scope, period, role, display_order)
			VALUES (?, ?, ?, ?, ?, ?)
		`, b.Org, b.Full, b.Scope, b.Period, b.Role, bIdx+1)
		require.NoError(t, err)
		bodyID, _ := res.LastInsertId()

		for _, p := range b.Projects {
			for _, d := range p.Documents {
				_, err := tx.Exec(`
					INSERT INTO standard_documents (body_id, wg, project_name, title, doc_ref, status, published_at)
					VALUES (?, ?, ?, ?, ?, ?, ?)
				`, bodyID, p.WG, p.Name, d.Title, d.Ref, d.Status, d.PublishedAt)
				require.NoError(t, err)
			}
		}
	}

	require.NoError(t, tx.Commit())
}

type APIResponse[T any] struct {
	Data  T `json:"data"`
	Error *struct {
		Message string `json:"message"`
	} `json:"error"`
}

func ptrString(s string) *string {
	return &s
}

func doRequest(r http.Handler, method, target string, body any, token string) *httptest.ResponseRecorder {
	var reqBody []byte
	if body != nil {
		reqBody, _ = json.Marshal(body)
	}
	req := httptest.NewRequest(method, target, bytes.NewReader(reqBody))
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	return w
}

func TestHealthCheck(t *testing.T) {
	db := setupTestDB(t)
	defer db.Close()
	r := router.SetupRouter(db, testAdminToken)

	w := doRequest(r, "GET", "/api/v1/health", nil, "")
	assert.Equal(t, http.StatusOK, w.Code)
}

func TestAdminAuthMiddleware(t *testing.T) {
	db := setupTestDB(t)
	defer db.Close()

	// A. Configured with explicit valid token
	r := router.SetupRouter(db, testAdminToken)

	// 1. Unauthenticated request to /admin/verify should return 401
	wNoAuth := doRequest(r, "GET", "/api/v1/admin/verify", nil, "")
	assert.Equal(t, http.StatusUnauthorized, wNoAuth.Code)

	// 2. Guessable fallback token "admin123" must return 401
	wGuessAuth := doRequest(r, "GET", "/api/v1/admin/verify", nil, "admin123")
	assert.Equal(t, http.StatusUnauthorized, wGuessAuth.Code)

	// 3. Wrong token request to /admin/verify should return 401
	wWrongAuth := doRequest(r, "GET", "/api/v1/admin/verify", nil, "wrong-token")
	assert.Equal(t, http.StatusUnauthorized, wWrongAuth.Code)

	// 4. Valid token request to /admin/verify should return 200
	wValidAuth := doRequest(r, "GET", "/api/v1/admin/verify", nil, testAdminToken)
	assert.Equal(t, http.StatusOK, wValidAuth.Code)

	// 5. Public GET routes remain accessible without auth
	wPublic := doRequest(r, "GET", "/api/v1/members", nil, "")
	assert.Equal(t, http.StatusOK, wPublic.Code)

	// B. Unconfigured (empty string) admin token: must strictly reject ALL mutating/verify attempts
	rEmptyToken := router.SetupRouter(db, "")

	// 6. Any token on verify should return 401
	wEmpty1 := doRequest(rEmptyToken, "GET", "/api/v1/admin/verify", nil, "admin123")
	assert.Equal(t, http.StatusUnauthorized, wEmpty1.Code)

	wEmpty2 := doRequest(rEmptyToken, "GET", "/api/v1/admin/verify", nil, "")
	assert.Equal(t, http.StatusUnauthorized, wEmpty2.Code)

	// 7. Mutating POST on empty router should return 401
	newProj := models.ResearchProject{
		Slug:     "test-unauth",
		Title:    "Unauthorized Title",
		Abstract: "Unauthorized abstract.",
		Keywords: []string{"unauth"},
	}
	wMutate := doRequest(rEmptyToken, "POST", "/api/v1/research-projects", newProj, "admin123")
	assert.Equal(t, http.StatusUnauthorized, wMutate.Code)

	// 8. Public GET routes still work on empty admin token router
	wPublicEmpty := doRequest(rEmptyToken, "GET", "/api/v1/health", nil, "")
	assert.Equal(t, http.StatusOK, wPublicEmpty.Code)
}

func TestResearchProjectsCRUD(t *testing.T) {
	db := setupTestDB(t)
	defer db.Close()
	r := router.SetupRouter(db, testAdminToken)

	// 1. Create Project
	newProj := models.ResearchProject{
		Slug:     "test-crud-proj",
		Title:    "Test Project Title",
		Abstract: "Test project abstract for CRUD verification.",
		Keywords: []string{"test", "crud"},
	}
	wCreate := doRequest(r, "POST", "/api/v1/research-projects", newProj, testAdminToken)
	assert.Equal(t, http.StatusCreated, wCreate.Code)

	var createResp APIResponse[models.ResearchProject]
	require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
	createdID := createResp.Data.ID
	assert.True(t, createdID > 0)

	// 2. Update Project
	newProj.Title = "Updated Project Title"
	wUpdate := doRequest(r, "PUT", "/api/v1/research-projects/"+strconvItoa(createdID), newProj, testAdminToken)
	assert.Equal(t, http.StatusOK, wUpdate.Code)

	// 3. Delete Project
	wDelete := doRequest(r, "DELETE", "/api/v1/research-projects/"+strconvItoa(createdID), nil, testAdminToken)
	assert.Equal(t, http.StatusNoContent, wDelete.Code)
}

func TestResearchAreasDisplayOrderShift(t *testing.T) {
	db := setupTestDB(t)
	defer db.Close()
	r := router.SetupRouter(db, testAdminToken)

	// Fetch initial research areas
	wInit := doRequest(r, "GET", "/api/v1/research-areas", nil, "")
	assert.Equal(t, http.StatusOK, wInit.Code)
	var initResp APIResponse[[]models.ResearchArea]
	require.NoError(t, json.Unmarshal(wInit.Body.Bytes(), &initResp))
	initAreas := initResp.Data
	require.NotEmpty(t, initAreas)

	// Target display_order = 2 (should shift existing 2..N to 3..N+1)
	newArea := models.ResearchArea{
		NameEn:       "Quantum Internet Protocols",
		NameKr:       ptrString("양자 인터넷 프로토콜"),
		DisplayOrder: 2,
	}

	wCreate := doRequest(r, "POST", "/api/v1/research-areas", newArea, testAdminToken)
	assert.Equal(t, http.StatusCreated, wCreate.Code)
	var createResp APIResponse[models.ResearchArea]
	require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
	createdID := createResp.Data.ID
	assert.Equal(t, 2, createResp.Data.DisplayOrder)

	// Fetch updated list and assert display_order shifted correctly
	wAfter := doRequest(r, "GET", "/api/v1/research-areas", nil, "")
	assert.Equal(t, http.StatusOK, wAfter.Code)
	var afterResp APIResponse[[]models.ResearchArea]
	require.NoError(t, json.Unmarshal(wAfter.Body.Bytes(), &afterResp))
	afterAreas := afterResp.Data

	assert.Equal(t, len(initAreas)+1, len(afterAreas))
	assert.Equal(t, "Quantum Internet Protocols", afterAreas[1].NameEn)
	assert.Equal(t, 2, afterAreas[1].DisplayOrder)
	assert.Equal(t, initAreas[1].NameEn, afterAreas[2].NameEn)
	assert.Equal(t, 3, afterAreas[2].DisplayOrder)

	// Clean up
	wDel := doRequest(r, "DELETE", "/api/v1/research-areas/"+strconvItoa(createdID), nil, testAdminToken)
	assert.Equal(t, http.StatusNoContent, wDel.Code)
}

func TestMembersAndAlumniCRUD(t *testing.T) {
	db := setupTestDB(t)
	defer db.Close()
	r := router.SetupRouter(db, testAdminToken)

	// 1. Create Member with Invalid Degree -> 400
	badMember := models.Member{
		NameKo: "홍길동",
		NameEn: "Gildong Hong",
		Degree: "InvalidDegree",
	}
	wBad := doRequest(r, "POST", "/api/v1/members", badMember, testAdminToken)
	assert.Equal(t, http.StatusBadRequest, wBad.Code)

	// 2. Create Valid Member -> 201
	validMember := models.Member{
		NameKo: "홍길동",
		NameEn: "Gildong Hong",
		Degree: "PhDStudent",
	}
	wCreate := doRequest(r, "POST", "/api/v1/members", validMember, testAdminToken)
	assert.Equal(t, http.StatusCreated, wCreate.Code)

	var createResp APIResponse[models.Member]
	require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
	mID := createResp.Data.ID

	// 3. Delete Member -> 204
	wDelete := doRequest(r, "DELETE", "/api/v1/members/"+strconvItoa(mID), nil, testAdminToken)
	assert.Equal(t, http.StatusNoContent, wDelete.Code)

	// 4. Create Alumnus -> 201
	validAlumnus := models.Alumnus{
		NameKo:      "김졸업",
		NameEn:      "Grad Kim",
		Degree:      "MS",
		GraduatedAt: "2024-02",
	}
	wCreateAl := doRequest(r, "POST", "/api/v1/alumni", validAlumnus, testAdminToken)
	assert.Equal(t, http.StatusCreated, wCreateAl.Code)

	var alResp APIResponse[models.Alumnus]
	require.NoError(t, json.Unmarshal(wCreateAl.Body.Bytes(), &alResp))
	alID := alResp.Data.ID

	// 5. Delete Alumnus -> 204
	wDelAl := doRequest(r, "DELETE", "/api/v1/alumni/"+strconvItoa(alID), nil, testAdminToken)
	assert.Equal(t, http.StatusNoContent, wDelAl.Code)
}

func TestCascadeDeletes(t *testing.T) {
	db := setupTestDB(t)
	defer db.Close()
	r := router.SetupRouter(db, testAdminToken)

	// 1. Create a semester
	newSem := models.Semester{Year: 2030, Term: "Spring"}
	wSem := doRequest(r, "POST", "/api/v1/semesters", newSem, testAdminToken)
	assert.Equal(t, http.StatusCreated, wSem.Code)
	var semResp APIResponse[models.Semester]
	require.NoError(t, json.Unmarshal(wSem.Body.Bytes(), &semResp))
	semID := semResp.Data.ID

	// 2. Create courses under this semester
	course1 := models.Course{Code: "TEST101", NameEn: "Intro to Testing"}
	course2 := models.Course{Code: "TEST102", NameEn: "Advanced Testing"}
	wCourse1 := doRequest(r, "POST", "/api/v1/semesters/"+strconvItoa(semID)+"/courses", course1, testAdminToken)
	assert.Equal(t, http.StatusCreated, wCourse1.Code)
	wCourse2 := doRequest(r, "POST", "/api/v1/semesters/"+strconvItoa(semID)+"/courses", course2, testAdminToken)
	assert.Equal(t, http.StatusCreated, wCourse2.Code)

	// Verify courses exist in DB
	var courseCount int
	err := db.QueryRow("SELECT COUNT(*) FROM courses WHERE semester_id = ?", semID).Scan(&courseCount)
	require.NoError(t, err)
	assert.Equal(t, 2, courseCount)

	// 3. Delete semester
	wDelSem := doRequest(r, "DELETE", "/api/v1/semesters/"+strconvItoa(semID), nil, testAdminToken)
	assert.Equal(t, http.StatusNoContent, wDelSem.Code)

	// 4. Verify cascade delete removed courses
	err = db.QueryRow("SELECT COUNT(*) FROM courses WHERE semester_id = ?", semID).Scan(&courseCount)
	require.NoError(t, err)
	assert.Equal(t, 0, courseCount, "Courses should be cascaded when parent semester is deleted")
}

func strconvItoa(i int) string {
	return strconv.Itoa(i)
}
