import { describe, it, expect } from 'vitest'
import { analyzeProse } from './prose'

// These tests pin the rules-based prose checker's behavior. For an ESL writer a
// *wrong* nudge costs trust faster than a missed one earns it, so every rule is
// tested in two directions: a true-positive case that must fire, and at least
// one near-miss "guard" case that must stay silent. Sentences are padded past
// the 8-word floor that analyzeProse needs before it offers advice.

const rulesFor = (text: string) => analyzeProse(text).map((h) => h.rule)

// Assert a given rule appears among the hints for this text.
function expectRule(text: string, rule: string) {
  expect(rulesFor(text), `expected "${rule}" for: ${text}`).toContain(rule)
}

// Assert the text produces no hints at all (false-positive guard).
function expectClean(text: string) {
  expect(analyzeProse(text), `expected no hints for: ${text}`).toEqual([])
}

describe('analyzeProse — floor', () => {
  it('stays quiet on very short drafts (under 8 English words)', () => {
    expect(analyzeProse('he go now')).toEqual([])
  })

  it('returns hints highest-priority first', () => {
    // This 36-word run-on also contains a lowercase "i"; run-on must lead.
    const hints = analyzeProse(
      'yesterday i walked all the way to the market and i bought apples and oranges and i saw my friend there and we talked and laughed and walked back home together in the warm afternoon sun.',
    )
    expect(hints[0].rule).toBe('runon')
    expect(hints.map((h) => h.rule)).toContain('cap-i')
  })
})

describe('run-ons', () => {
  it('flags a long, comma-heavy sentence', () => {
    expectRule(
      'I went to the market and I bought some apples and then I saw my friend and we talked for a while and after that we went to the cafe and ordered coffee and cake together.',
      'runon',
    )
  })

  it('leaves a normal sentence alone', () => {
    expectClean('The garden was quiet in the morning and the rain fell softly on the leaves.')
  })
})

describe('comma splices', () => {
  it('flags two independent clauses joined by a comma', () => {
    expectRule('I finished the report, I sent it to my boss right away today.', 'splice')
  })

  it('catches a splice with a verb not on any list', () => {
    expectRule('The garden was quiet in the morning, she poured a cup of tea slowly.', 'splice')
  })

  it('does not flag an introductory transition (However, …)', () => {
    expectClean('However, we decided to stay home and rest for the entire weekend.')
  })

  it('does not flag an introductory phrase (After dinner, …)', () => {
    expectClean('After dinner, I went home and read a book until I fell asleep.')
  })

  it('does not flag a pronoun list after a comma', () => {
    expectClean('We invited my brother, you and your sister to the dinner party.')
  })
})

describe('unclear antecedents', () => {
  it('flags a vague "This + verb" opener after a prior sentence', () => {
    expectRule(
      'We changed the whole schedule last week. This makes everyone confused about the new times.',
      'antecedent',
    )
  })

  it('leaves "This" alone when it names its noun', () => {
    expectClean(
      'We changed the whole schedule last week. This new schedule confuses everyone about the times.',
    )
  })
})

describe('oxford comma', () => {
  it('flags a 3-item noun list missing the serial comma', () => {
    expectRule('My mother, my father and my sister came to visit us last summer.', 'oxford')
  })

  it('flags a verb-phrase list missing the serial comma', () => {
    expectRule('We cleaned the kitchen, washed the dishes and took out the trash.', 'oxford')
  })

  it('does not flag a list that already has the serial comma', () => {
    expectClean('I bought apples, bananas, and oranges at the store this morning.')
  })

  it('does not flag a compound predicate as a list', () => {
    expectClean('After dinner, I went home and read a book until I fell asleep.')
  })
})

describe('a / an', () => {
  it('flags "a" before a vowel sound', () => {
    expectRule('She found a umbrella and a old book on the table by the window.', 'article')
  })

  it('flags "an" before a consonant sound', () => {
    expectRule('He gave me an book and an pencil to use during the long exam.', 'article')
  })

  it('respects sound exceptions (a university, an hour)', () => {
    expectClean('He is a university student and it took an hour to arrive here.')
  })
})

describe('uncountable plurals', () => {
  it('flags a pluralized mass noun', () => {
    expectRule('She gave me many informations about the new job opening today.', 'uncountable')
  })

  it('leaves the singular mass noun alone', () => {
    expectClean('He shared some useful advice and helped me with my homework tonight.')
  })
})

describe('capitalization of proper nouns', () => {
  it('flags lowercase languages and days', () => {
    expectRule('I am learning english and french during my free time every week.', 'propercap')
    expectRule('We will meet on monday to discuss the final project together soon.', 'propercap')
  })

  it('does not flag ambiguous homographs (may, march)', () => {
    expectClean('They may visit in the spring and march through the old town square.')
  })

  it('leaves already-capitalized proper nouns alone', () => {
    expectClean('I will study English and Chinese together this coming summer break here.')
  })
})

describe('subject–verb agreement', () => {
  it('flags a bare verb after he/she/it', () => {
    expectRule('My brother is busy but he go to the gym every single morning.', 'sva')
    expectRule('She have a lovely garden full of roses behind her small house.', 'sva')
  })

  it('flags a sentence-initial subject too', () => {
    expectRule('It make me happy whenever the sun comes out after the rain.', 'sva')
  })

  it('does not flag causative/perception frames (let it go, make it work)', () => {
    expectClean('Please let it go and try to enjoy the rest of your day.')
    expectClean('We should make it work before the big meeting tomorrow afternoon.')
  })
})

describe('plural after a number', () => {
  it('flags a singular noun after a cardinal number', () => {
    expectRule('I bought three apple and some bread at the market this morning.', 'plural')
  })

  it('does not flag an adjective between number and noun', () => {
    expectClean('We adopted two small dogs from the shelter near our apartment today.')
  })

  it('does not flag irregular plurals (five people)', () => {
    expectClean('There were five people waiting patiently outside in the cold morning rain.')
  })

  it('does not flag an already-plural noun', () => {
    expectClean('She has two cats and they sleep all day long on the couch.')
  })
})

describe('double determiner', () => {
  it('flags a stacked article + possessive', () => {
    expectRule('Please put the my book back on the shelf when you finish it.', 'doubledet')
  })
})

describe('there is + plural', () => {
  it('flags "there is" with a plural quantifier', () => {
    expectRule('There is many reasons to visit the coast during the warm months.', 'thereis')
  })

  it('does not flag "there is some" with a mass noun', () => {
    expectClean('There is some water left in the bottle if you are thirsty now.')
  })
})

describe('its / it’s', () => {
  it("flags it's used as a possessive", () => {
    expectRule("The cat licked it's own paw while sitting quietly by the window.", 'its')
  })

  it('flags its used where "it is" is meant', () => {
    expectRule('I think its a wonderful idea to travel together next year sometime.', 'its')
  })

  it('leaves correct possessive "its" alone', () => {
    expectClean('The dog wagged its tail happily when we came home that evening.')
  })
})

describe('than / then', () => {
  it('flags a comparative followed by "then"', () => {
    expectRule('This cake tastes much better then the one we made last weekend.', 'than')
  })

  it('leaves sequential "then" alone', () => {
    expectClean('We ate dinner and then we watched a long movie on the sofa.')
  })
})

describe('comma after a transition', () => {
  it('flags a sentence-initial transition with no comma', () => {
    expectRule('However we decided to stay home and rest for the whole weekend.', 'introcomma')
  })

  it('does not flag a sequence word (Then …)', () => {
    expectClean('Then we walked to the park and fed the ducks by the pond.')
  })
})

describe('spacing around punctuation', () => {
  it('flags a missing space after a comma', () => {
    expectRule('I like apples,bananas and grapes from the little shop down the street.', 'space-after')
  })

  it('flags a space before punctuation', () => {
    expectRule('She smiled warmly , then walked away without saying a single word today.', 'space-punct')
  })

  it('does not flag thousands separators in numbers', () => {
    expectClean('The list had 1,000 items and cost us about 2,500 dollars in total.')
  })
})

describe('capitalization basics', () => {
  it('flags a lowercase standalone "i"', () => {
    expectRule('Yesterday i walked to the store and bought milk, eggs, and bread.', 'cap-i')
  })

  it('flags a sentence that does not start with a capital', () => {
    expectRule('The sun set slowly. it was a beautiful evening down by the river.', 'cap-sentence')
  })
})

import { mechanicsFindings } from './prose'

// mechanicsFindings is the applyable subset feeding the suggestion cards. Each
// finding must carry an EXACT span (text.slice(from,to) === original) and a
// replacement that actually changes the text — these become one-click edits.
describe('mechanicsFindings — applyable fixes', () => {
  // Every finding's span must be exact, regardless of which rule produced it.
  function expectExactSpans(text: string) {
    const found = mechanicsFindings(text)
    for (const f of found) {
      expect(text.slice(f.from, f.to), `span for ${JSON.stringify(f)}`).toBe(f.original)
      expect(f.replacement).not.toBe(f.original)
    }
    return found
  }

  const one = (text: string, rulePred: (f: ReturnType<typeof mechanicsFindings>[number]) => boolean) =>
    expectExactSpans(text).find(rulePred)

  it('doubled word → single word, exact span', () => {
    const f = one('I saw the the cat in the garden today.', (f) => f.original === 'the the')
    expect(f?.replacement).toBe('the')
  })

  it('finds BOTH doublings with distinct spans', () => {
    const found = mechanicsFindings('the the dog and the the cat ran around the yard.')
      .filter((f) => f.original === 'the the')
    expect(found).toHaveLength(2)
    expect(found[0].from).not.toBe(found[1].from)
  })

  // Spans are widened to a distinctive phrase so they re-anchor by string in the
  // editor (a bare "a"/"i"/" is" would match the wrong spot). The replacement
  // carries the whole corrected phrase.

  it('lowercase standalone i is awareness-only (no card)', () => {
    // It still flags as a companion bubble (see analyzeProse), but a single
    // character can't be anchored, so it must NOT become a card.
    expect(mechanicsFindings('Yesterday i walked to the store and came home.').length).toBe(0)
    expect(rulesFor('Yesterday i walked to the store and came home.')).toContain('cap-i')
  })

  it('a → an: spans the whole "a <noun>" phrase', () => {
    const f = one('She found a umbrella under the old wooden table.', (f) => f.original === 'a umbrella')
    expect(f?.replacement).toBe('an umbrella')
  })

  it('uncountable plural → singular', () => {
    const f = one('She gave me many informations about the new job.', (f) => f.original === 'informations')
    expect(f?.replacement).toBe('information')
  })

  it('lowercase proper noun → capitalized', () => {
    const f = one('I am learning english during my free time this year.', (f) => f.original === 'english')
    expect(f?.replacement).toBe('English')
  })

  it('subject-verb agreement → corrects the verb, spans subject+verb', () => {
    const f = one('She have three cats and a dog at her house.', (f) => f.original === 'She have')
    expect(f?.replacement).toBe('She has')
  })

  it('singular noun after a number → plural, spans number+noun', () => {
    const f = one('I bought five apple at the market this morning.', (f) => f.original === 'five apple')
    expect(f?.replacement).toBe('five apples')
  })

  it('comparative + then → than, spans the phrase', () => {
    const f = one('This book is better then the one I read last week.', (f) => f.original === 'better then')
    expect(f?.replacement).toBe('better than')
  })

  it('space before punctuation is removed (spans the word)', () => {
    const f = one('I love it , it makes me very happy indeed.', (f) => f.original === 'it ,')
    expect(f?.replacement).toBe('it,')
  })

  it('missing space after a comma is added (spans both words)', () => {
    const f = one('I bought apples,bananas and pears at the store.', (f) => f.original === 'apples,bananas')
    expect(f?.replacement).toBe('apples, bananas')
  })

  it('stacked determiner drops the article', () => {
    const f = one('She left the my book on the kitchen table again.', (f) => f.original === 'the my')
    expect(f?.replacement).toBe('my')
  })

  it('there is + plural → there are, spans the phrase', () => {
    const f = one('There is many people waiting outside in the cold.', (f) => f.original === 'There is many')
    expect(f?.replacement).toBe('There are many')
  })

  it("it's own → its own, spans the phrase", () => {
    const f = one("The cat licked it's own paw very slowly today.", (f) => f.original === "it's own")
    expect(f?.replacement).toBe('its own')
  })

  it("its a → it's a, spans the phrase", () => {
    const f = one('I think its a wonderful day to go outside now.', (f) => f.original === 'its a')
    expect(f?.replacement).toBe("it's a")
  })

  // Awareness-only rules never produce cards — they stay companion bubbles.
  it('run-ons and splices produce NO card findings', () => {
    const runOn = 'I went to the market and I bought some apples and then I saw my friend and we talked for a while and after that we went to the cafe and ordered coffee and cake together.'
    expect(mechanicsFindings(runOn).some((f) => f.original.length > 30)).toBe(false)
    const splice = 'I finished the report, I sent it to my boss right away today.'
    // no card spans the comma-join; only (if any) small word-level fixes
    expect(mechanicsFindings(splice).every((f) => f.to - f.from < 12)).toBe(true)
  })

  it('clean prose yields no findings', () => {
    expect(mechanicsFindings('The garden was quiet and the rain fell softly on the leaves.')).toEqual([])
  })
})
