import { beforeEach, describe, expect, it, vi } from 'vitest'

import { apiGet, apiPost } from '@/lib/api'
import {
  buildGekanatorQuestions,
  expectedAnswerForQuestion,
  fetchGekanatorPosts,
  fetchGekanatorQuestions,
  learnedSemanticSideForPost,
  questionIdForCondition,
  restoreGekanatorQuestion,
  saveGekanatorExtraQuestionAnswers,
  saveGekanatorGame,
  saveGekanatorQuestionSuggestion,
} from '@/lib/gekanator'

import type {
  GekanatorAnswerLog,
  StoredGekanatorQuestion,
} from '@/lib/gekanator'
import type { Post } from '@/types'

vi.mock('@/lib/api', () => ({
  apiGet: vi.fn(),
  apiPost: vi.fn(),
}))

const mockedApiPost = vi.mocked(apiPost)
const mockedApiGet = vi.mocked(apiGet)

const post = (overrides: Partial<Post> = {}): Post => ({
  id: 1,
  versionNo: 1,
  url: 'https://example.com/posts/1',
  title: 'post title',
  thumbnail: null,
  thumbnailBase: null,
  tags: [],
  viewed: false,
  related: [],
  originalCreatedFrom: null,
  originalCreatedBefore: null,
  createdAt: '2026-06-10T00:00:00.000Z',
  updatedAt: '2026-06-10T00:00:00.000Z',
  uploadedUser: null,
  ...overrides,
})

describe('Gekanator API functions', () => {
  it('returns posts from the Gekanator posts endpoint', async () => {
    const posts = [post()]
    mockedApiGet.mockResolvedValueOnce({ posts })

    await expect(fetchGekanatorPosts()).resolves.toEqual(posts)
    expect(mockedApiGet).toHaveBeenCalledWith('/gekanator/posts')
  })

  it('returns questions from the Gekanator questions endpoint', async () => {
    const questions: StoredGekanatorQuestion[] = []
    mockedApiGet.mockResolvedValueOnce({ questions })

    await expect(fetchGekanatorQuestions()).resolves.toEqual(questions)
    expect(mockedApiGet).toHaveBeenCalledWith('/gekanator/questions')
  })
})

describe('expectedAnswerForQuestion', () => {
  it('returns a direct example answer when present', () => {
    const question: StoredGekanatorQuestion = {
      id: 'post-similarity:10',
      text: '喜多ちゃんが泣いてる?',
      kind: 'post_similarity',
      source: 'user_suggested',
      priorityWeight: 1.2,
      condition: {
        type: 'post-similarity',
        postId: 999,
        answer: 'yes',
        threshold: 0.65,
      },
      exampleAnswers: {
        1: 'partial',
      },
    }

    expect(expectedAnswerForQuestion(question, post({ id: 1 }))).toBe('partial')
  })

  it('returns the condition answer for the original post_similarity post', () => {
    const question: StoredGekanatorQuestion = {
      id: 'post-similarity:10',
      text: '喜多ちゃんが泣いてる?',
      kind: 'post_similarity',
      source: 'user_suggested',
      priorityWeight: 1.2,
      condition: {
        type: 'post-similarity',
        postId: 123,
        answer: 'probably_no',
        threshold: 0.65,
      },
    }

    expect(expectedAnswerForQuestion(question, post({ id: 123 }))).toBe('probably_no')
  })

  it('returns null for an unrelated post_similarity post without examples', () => {
    const question: StoredGekanatorQuestion = {
      id: 'post-similarity:10',
      text: '喜多ちゃんが泣いてる?',
      kind: 'post_similarity',
      source: 'user_suggested',
      priorityWeight: 1.2,
      condition: {
        type: 'post-similarity',
        postId: 123,
        answer: 'yes',
        threshold: 0.65,
      },
    }

    expect(expectedAnswerForQuestion(question, post({ id: 456 }))).toBeNull()
  })

  it('returns yes for a matching tag question', () => {
    const question: StoredGekanatorQuestion = {
      id: 'tag:character:喜多郁代',
      text: '喜多ちゃんが関係してる?',
      kind: 'tag',
      condition: {
        type: 'tag',
        key: 'character:喜多郁代',
      },
    }

    expect(
      expectedAnswerForQuestion(
        question,
        post({
          tags: [
            {
              id: 1,
              name: '喜多郁代',
              category: 'character',
              aliases: [],
              parents: [],
              postCount: 1,
              createdAt: '2026-06-10T00:00:00.000Z',
              updatedAt: '2026-06-10T00:00:00.000Z',
              deprecatedAt: null,
              hasWiki: false,
              hasDeerjikists: false,
              materialId: null,
            },
          ],
        }),
      ),
    ).toBe('yes')
  })

  it('returns no for a non-matching tag question', () => {
    const question: StoredGekanatorQuestion = {
      id: 'tag:character:喜多郁代',
      text: '喜多ちゃんが関係してる?',
      kind: 'tag',
      condition: {
        type: 'tag',
        key: 'character:喜多郁代',
      },
    }

    expect(expectedAnswerForQuestion(question, post({ tags: [] }))).toBe('no')
  })

  it('ignores example answers for direct title facts', () => {
    const question: StoredGekanatorQuestion = {
      id: 'title:length-at-least:20',
      text: 'タイトルは 20 文字以上?',
      kind: 'title',
      condition: {
        type: 'title-length-at-least',
        length: 20,
      },
      exampleAnswers: {
        1: 'yes',
      },
    }

    expect(expectedAnswerForQuestion(question, post({ id: 1, title: 'short' }))).toBe('no')
  })

  it('returns yes for matching title-contains questions', () => {
    const question: StoredGekanatorQuestion = {
      id: 'title:contains:結束バンド',
      text: '題名に「結束バンド」が含まれる?',
      kind: 'title',
      condition: {
        type: 'title-contains',
        text: '結束バンド',
      },
    }

    expect(expectedAnswerForQuestion(
      question,
      post({ title: '結束バンドのライブ' }),
    )).toBe('yes')
    expect(expectedAnswerForQuestion(
      question,
      post({ title: '後藤ひとりの休日' }),
    )).toBe('no')
  })
})

describe('learnedSemanticSideForPost', () => {
  it('classifies post_similarity examples as positive, negative, or unknown', () => {
    const question: StoredGekanatorQuestion = {
      id: 'post-similarity:10',
      text: '喜多ちゃんが泣いてる?',
      kind: 'post_similarity',
      source: 'user_suggested',
      priorityWeight: 1.2,
      condition: {
        type: 'post-similarity',
        postId: 123,
        answer: 'partial',
        threshold: 0.65,
      },
      exampleAnswers: {
        1: 'yes',
        2: 'probably_no',
      },
    }

    expect(learnedSemanticSideForPost(question, post({ id: 1 }))).toBe('positive')
    expect(learnedSemanticSideForPost(question, post({ id: 2 }))).toBe('negative')
    expect(learnedSemanticSideForPost(question, post({ id: 3 }))).toBe('unknown')
    expect(learnedSemanticSideForPost(question, post({ id: 123 }))).toBe('positive')
  })
})

describe('restoreGekanatorQuestion', () => {
  it('uses default source and priority weight when omitted', () => {
    const question = restoreGekanatorQuestion({
      id: 'tag:character:喜多郁代',
      text: '喜多ちゃんが関係してる?',
      kind: 'tag',
      condition: {
        type: 'tag',
        key: 'character:喜多郁代',
      },
    })

    expect(question.source).toBe('default')
    expect(question.priorityWeight).toBe(1)
  })

  it('tests a post_similarity question using direct examples', () => {
    const question = restoreGekanatorQuestion({
      id: 'post-similarity:10',
      text: '喜多ちゃんが泣いてる?',
      kind: 'post_similarity',
      source: 'user_suggested',
      priorityWeight: 1.2,
      condition: {
        type: 'post-similarity',
        postId: 999,
        answer: 'yes',
        threshold: 0.65,
      },
      exampleAnswers: {
        1: 'yes',
        2: 'no',
      },
    })

    expect(question.test(post({ id: 1 }))).toBe(true)
    expect(question.test(post({ id: 2 }))).toBe(false)
    expect(question.test(post({ id: 3 }))).toBe(false)
  })

  it('tests a post_similarity question against its configured partial answer', () => {
    const question = restoreGekanatorQuestion({
      id: 'post-similarity:10',
      text: '喜多ちゃんが泣いてる?',
      kind: 'post_similarity',
      source: 'user_suggested',
      priorityWeight: 1.2,
      condition: {
        type: 'post-similarity',
        postId: 999,
        answer: 'partial',
        threshold: 0.65,
      },
      exampleAnswers: {
        1: 'partial',
        2: 'yes',
      },
    })

    expect(question.test(post({ id: 1 }))).toBe(true)
    expect(question.test(post({ id: 2 }))).toBe(true)
  })

  it('normalizes legacy title-length-greater-than questions', () => {
    const question = restoreGekanatorQuestion({
      id: 'title:length-greater-than:20',
      text: '題名が長めの投稿?',
      kind: 'title',
      condition: {
        type: 'title-length-greater-than',
        length: 20,
      },
    })

    expect(question.id).toBe('title:length-at-least:21')
    expect(question.condition).toEqual({
      type: 'title-length-at-least',
      length: 21,
    })
    expect(question.test(post({ title: 'x'.repeat(20) }))).toBe(false)
    expect(question.test(post({ title: 'x'.repeat(21) }))).toBe(true)
  })

  it('restores title-contains questions with a title matcher', () => {
    const question = restoreGekanatorQuestion({
      id: 'title:contains:結束バンド',
      text: '題名に「結束バンド」が含まれる?',
      kind: 'title',
      condition: {
        type: 'title-contains',
        text: '結束バンド',
      },
    })

    expect(question.test(post({ title: '結束バンドのライブ' }))).toBe(true)
    expect(question.test(post({ title: '後藤ひとりの休日' }))).toBe(false)
  })
})

describe('buildGekanatorQuestions', () => {
  it('builds quantitative title length questions', () => {
    const questions = buildGekanatorQuestions([
      post({ id: 1, title: 'a' }),
      post({ id: 2, title: 'bb' }),
      post({ id: 3, title: 'ccc' }),
      post({ id: 4, title: 'dddd' }),
    ])
    const titleQuestion = questions.find(question =>
      question.condition.type === 'title-length-at-least')

    expect(titleQuestion?.text).toMatch(/^タイトルは \d+ 文字以上\?$/)
    expect(titleQuestion?.id).toMatch(/^title:length-at-least:\d+$/)
  })

  it('builds title-contains questions from repeated title words', () => {
    const questions = buildGekanatorQuestions([
      post({ id: 1, title: '結束バンド ライブ' }),
      post({ id: 2, title: '結束バンド 新曲' }),
      post({ id: 3, title: '後藤ひとり 練習' }),
      post({ id: 4, title: '伊地知虹夏 練習' }),
    ])

    const titleContainsQuestion = questions.find(question =>
      question.condition.type === 'title-contains'
      && question.condition.text === '結束バンド')

    expect(titleContainsQuestion).toMatchObject({
      id: 'title:contains:結束バンド',
      text: '題名に「結束バンド」が含まれる?',
      kind: 'title',
      source: 'default',
      priorityWeight: .96,
    })
    expect(titleContainsQuestion?.test(post({ title: '結束バンドのライブ' }))).toBe(true)
    expect(titleContainsQuestion?.test(post({ title: '廣井きくりのライブ' }))).toBe(false)
  })

  it('honors question caps and title-contains toggles', () => {
    const posts = [
      post({ id: 1, title: '結束バンド ライブ' }),
      post({ id: 2, title: '結束バンド 新曲' }),
      post({ id: 3, title: '後藤ひとり 練習' }),
      post({ id: 4, title: '伊地知虹夏 練習' }),
    ]

    const capped = buildGekanatorQuestions(posts, {
      titleContainsCap: 1,
      totalQuestionCap: 1,
    })
    const withoutTitleContains = buildGekanatorQuestions(posts, {
      includeTitleContains: false,
    })

    expect(capped).toHaveLength(1)
    expect(withoutTitleContains.some(question =>
      question.condition.type === 'title-contains')).toBe(false)
  })
})

describe('questionIdForCondition', () => {
  it('builds stable ids for title-contains questions', () => {
    expect(questionIdForCondition({
      type: 'title-contains',
      text: '結束バンド',
    })).toBe('title:contains:結束バンド')
  })
})

describe('Gekanator API writers', () => {
  beforeEach(() => {
    mockedApiPost.mockReset()
  })

  it('sends game results using snake_case request keys', async () => {
    mockedApiPost.mockResolvedValue({ id: 100 })

    const answers: GekanatorAnswerLog[] = [
      {
        questionId: 'tag:character:喜多郁代',
        questionText: '喜多ちゃんが関係してる?',
        questionCondition: {
          type: 'tag',
          key: 'character:喜多郁代',
        },
        questionMode: 'normal',
        questionPurpose: 'effective_user_suggested',
        effectiveQuestion: true,
        learningQuestion: false,
        answer: 'yes',
        originalAnswer: 'partial',
      },
    ]

    await expect(
      saveGekanatorGame({
        guessedPostId: 1,
        correctPostId: 2,
        answers,
      }),
    ).resolves.toEqual({ id: 100 })

    expect(mockedApiPost).toHaveBeenCalledWith('/gekanator/games', {
      guessed_post_id: 1,
      correct_post_id: 2,
      answers: [
        {
          question_id: 'tag:character:喜多郁代',
          question_text: '喜多ちゃんが関係してる?',
          question_condition: {
            type: 'tag',
            key: 'character:喜多郁代',
          },
          question_mode: 'normal',
          question_purpose: 'effective_user_suggested',
          effective_question: true,
          learning_question: false,
          answer: 'yes',
          original_answer: 'partial',
        },
      ],
    })
  })

  it('sends question suggestions using snake_case request keys', async () => {
    mockedApiPost.mockResolvedValue({
      id: 10,
      count: 1,
    })

    await expect(
      saveGekanatorQuestionSuggestion({
        gekanatorGameId: 100,
        questionText: '喜多ちゃんが泣いてる?',
        answer: 'yes',
      }),
    ).resolves.toEqual({
      id: 10,
      count: 1,
    })

    expect(mockedApiPost).toHaveBeenCalledWith('/gekanator/question_suggestions', {
      gekanator_game_id: 100,
      question_text: '喜多ちゃんが泣いてる?',
      answer: 'yes',
    })
  })

  it('sends extra question answers using snake_case request keys', async () => {
    mockedApiPost.mockResolvedValue({
      count: 2,
    })

    await saveGekanatorExtraQuestionAnswers({
      gameId: 100,
      answers: [
        {
          questionId: 10,
          answer: 'yes',
        },
        {
          questionId: 11,
          answer: 'probably_no',
        },
      ],
    })

    expect(mockedApiPost).toHaveBeenCalledWith(
      '/gekanator/games/100/extra_question_answers',
      {
        answers: [
          {
            question_id: 10,
            answer: 'yes',
          },
          {
            question_id: 11,
            answer: 'probably_no',
          },
        ],
      },
    )
  })
})
